剑指offer链表题目汇总(面试必备)

本文介绍了链表相关的经典算法问题,包括反转链表、从尾到头打印链表、合并两个排序链表、寻找链表的第一个公共节点、链表中环的入口节点、删除链表重复节点、保留一个重复节点、找到链表倒数第k个节点、复杂链表的深拷贝以及LRU缓存实现。这些问题涉及到了链表的基本操作和数据结构优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

剑指offer链表题目汇总(C++版)

1、反转链表
输入一个链表,反转链表后,输出新链表的表头。
比较简单,直接上代码。

时间复杂度:O(n) 空间复杂度:O(1)

ListNode* ReverseList(ListNode* pHead) {
        if(!pHead) return pHead;
        ListNode* pre = nullptr;
        while(pHead)
        {
            ListNode* temp = pHead->next;
            pHead->next = pre;
            pre = pHead;
            pHead = temp;
        }
        return pre;
}

 
2、从尾到头打印链表
输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
思路1:遍历一遍链表将每个节点值存储到数组中,然后翻转数组。
时间复杂度:O(n) 空间复杂度:O(n)

vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> v;
        if(!head) return v;
        while(head)
        {
            v.push_back(head->val);
            head = head->next;
        }
        reverse(v.begin(), v.end());
        return v;
}

 
思路2:可以向将链表反转(反转链表),然后在遍历、存储、返回。

 
3、合并两个排序链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路1:非递归方法,定义一个虚拟头结点,然后遍历两个链表,逐个将较小的节点连上直到其中一个链表遍历完,再把另一个的链表没遍历完的部分连上。
时间复杂度:O(m+n),m,n分别为两个单链表的长度
空间复杂度:O(1)

ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        if(!pHead1) return pHead2;
        if(!pHead2) return pHead1;
        ListNode* pre = new ListNode(-1), *cur = pre;
        while(pHead1 && pHead2)
        {
            if(pHead1->val <= pHead2->val)
            {
                cur->next = pHead1;
                pHead1 = pHead1->next;
            }
            else 
            {
                cur->next = pHead2;
                pHead2 = pHead2->next;
            }
            cur = cur->next;
        }
        pHead1 == nullptr ? cur->next = pHead2 : cur->next = pHead1; //把剩余没遍历完的部分连上
        return pre->next;
}

 
思路2:递归方法
合并两个单链表,返回两个单链表头结点值小的那个节点。既然了解了这个函数功能,那么接下来需要考虑2个问题:
递归函数结束的条件是什么?
递归函数一定是缩小递归区间的,那么下一步的递归区间是什么?
对于问题1,对于链表就是,如果为空,返回什么
对于问题2,跟迭代方法中的一样,如果 pHead1 的所指节点值小于等于 pHead2 所指的结点值,那么pHead1 后续节点和 pHead2 节点继续递归。

时间复杂度:O(m+n)
空间复杂度:O(m+n),每一次递归,递归栈都会保存一个变量,最差情况会保存(m+n)个变量

ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
        if(!pHead1)
            return pHead2;
        if(!pHead2)
            return pHead1;
        if(pHead1->val<=pHead2->val)
        {
            pHead1->next=Merge(pHead1->next,pHead2);
            return pHead1;
        }
        else
        {
            pHead2->next=Merge(pHead1,pHead2->next);
            return pHead2;
        }
}

 
4、两个链表的第一个公共节点
输入两个无环的单链表,找出它们的第一个公共结点。(没有公共节点返回null)
ps:如果有公共结点肯定是在后面重叠,且后面部分都是共同的。
思路1:先计算出两个链表的长度,可以让比较长的先走两个链表长度之差的步数,然后两个再一起走看是否有公共节点。
时间复杂度:O(m+n) ,空间复杂度:O(1)

ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) {
    if (!pHead1 || !pHead2) return nullptr;
    int len1 = 0, len2 = 0;
    auto p = pHead1, q = pHead2;  
    for (auto t = pHead1; t; t = t->next) len1++;  //计算两个链表的长度
    for (auto t = pHead2; t; t = t->next) len2++;
    int k = len1 - len2;
    if (k < 0)
    {
        p = pHead2, q = pHead1;  //维护k代表长度差 p指向较长的链表 q指向较短的链表
        k = -k;
    }
    while (k--) p = p->next;    //长链表先走k步
    while (p != q)             //两个链表一起走直到相等
    {
        p = p->next;
        q = q->next;
    }
    return p;
}

 
思路2:让两条链表分别从各自的开头往后遍历,当其中一条遍历到末尾时,跳到另一个条链表的开头继续遍历。两个指针最终会相等,而且只有两种情况,一种情况是在交点处相等,另一种情况是在各自的末尾的空节点处相等。相等的原因是两个指针走过的路程都是两个链表的长度之和。
时间复杂度:O(m+n) ,空间复杂度:O(1)

ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) {
    if (!pHead1 || !pHead2) return nullptr;
    auto p = pHead1, q = pHead2;
    while (p != q)
    {
       p =  p ? p ->next : pHead2;
       q =  q ? q->next : pHead1;
    }
    return p;
}

 
5、链表中环的入口节点
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
思路1:利用哈希表
遍历链表,判断哈希表中是否已有当前遍历到的节点,没有就添加该节点到哈希表,否则返回该节点
时间复杂度:O(n) ,空间复杂度:O(n)

ListNode* EntryNodeOfLoop(ListNode* pHead) {
    if (!pHead) return pHead;
    unordered_set<ListNode*> s;
    while (pHead) {
        if (!s.count(pHead)) {
            s.insert(pHead);
            pHead = pHead->next;
        }
        else  return pHead;
    }
    return nullptr;
}

 
思路2:双指针算法
第一步,找环中相汇点。分别用p1,p2指向链表头部,p1每次走一步,p2 每次走二步,直到 p1==p2 找到在环中的相汇点。
第二步,找环的入口。接上步,当 p1==p2 时,p2 所经过节点数为2x,p1 所经过节点数为 x,设环中有n个节点,p2 比 p1 多走一圈有2x=n+x;n=x;可以看出 p1 实际走了一个环的步数,再让 p2 指向链表头部,p1位置不变,p1,p2 每次走一步直到 p1==p2;此时p1指向环的入口。
时间复杂度:O(n) ,空间复杂度:O(1)

ListNode* EntryNodeOfLoop(ListNode* pHead) {
    if (!pHead) return pHead;
    auto fast = pHead,slow = pHead;
    while (fast && fast->next) {
        fast = fast->next->next;
        slow = slow->next;
        if (fast == slow) {
            fast = pHead;
            while (fast!=slow) {
                fast = fast->next;
                slow = slow->next;
            }
            return fast;
        }
    }
    return nullptr;
}

 
6、删除链表中重复的节点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。
思路:链表开头可能会有重复项,被删掉的话头指针会改变,而最终却还需要返回链表的头指针。所以需要定义一个虚拟头节点,连上原链表,然后定义一个前驱指针和一个现指针,每当前驱指针指向新建的节点,现指针从下一个位置开始向后遍历,遇到相同的则继续往下,直到遇到不同项时,把前驱指针的 next 指向下面那个不同的元素。如果现指针遍历的第一个元素就不相同,则把前驱指针向下移一位。
时间复杂度:O(n) , 空间复杂度:O(1)

ListNode* deleteDuplication(ListNode* pHead) {
    if (!pHead) return pHead;
    auto dummyHead = new ListNode(-1);
    dummyHead->next = pHead;
    auto pre = dummyHead;
    while (pre->next)
    {
        ListNode* cur = pre->next;
        while (cur->next && cur->val == cur->next->val) cur = cur->next; //指向重复元素的最后一个
        if (cur != pre->next) pre->next = cur->next;//这里比较的是节点,不是值
        else
            pre = pre->next;
    }
    return  dummyHead->next;
}

模拟一下:比如说题目中的例子 -1 -> 1 -> 1 -> 1 -> 2 -> 3,此时,执行完内部的 while 循环后,cur 指向第三个1,pre 指向 -1,那么我们判断pre->next (指向第一个1)和 cur,是不相同的,虽然结点值相同,都是1,但确是不同的结点,所以 1 全要跳过,那么 -1 后面要直接连上2。但如果是下面这个链表:-1 -> 1 -> -> 2 -> 3。此时,执行完内部的while循环后,cur指向1,pre指向-1,那么我们判断pre->next和 cur 是相同的,所以不能跳过1,那么直接让 pre 向后移动一位即可。

 
7、删除链表中重复的节点且要保留重复节点中的一个
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点保留一个,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->3->4->5。
思路:遍历一遍链表,遇到当前节点值等于下一节点值的,直接让当前节点指向其下下个节点,否则,继续判断其下个节点。
时间复杂度:O(n) , 空间复杂度:O(1)

ListNode* deleteNode(ListNode* head)
{
    if (!head) return head;
    auto cur = head;
    while (cur && cur->next) {
        if (cur->val == cur->next->val) cur->next = cur->next->next;
        else cur = cur->next;
    }
    return head;
}

 
8、链表中倒数最后k个节点
输入一个链表,输出一个链表,该输出链表包含原链表中从倒数第k个结点至尾节点的全部节点。如果该链表长度小于k,请返回一个长度为 0 的链表。
思路1:可以向求出链表长度 len,然后用从头开始遍历链表走 len-k 步,返回当前节点即可(需要遍历两遍链表)
时间复杂度:O(n) , 空间复杂度:O(1)

ListNode* FindKthToTail(ListNode* pHead, int k) {
    if (!pHead) return pHead;
    ListNode* cur = pHead;
    int len = 0;
    while (cur) {
        len++;
        cur = cur->next;
    }
    if (k > len) return nullptr;
    int cnt = len - k;
    while (cnt) {
        pHead = pHead->next;
        cnt--;
    }
    return pHead;
}

 
思路2:双指针算法(遍历一遍链表),第一个指针先移动k步,然后第二个指针再从头开始,这个时候这两个指针同时移动,当第一个指针到链表的末尾的时候,返回第二个指针即可
时间复杂度:O(n) , 空间复杂度:O(1)

ListNode* FindKthToTail(ListNode* pHead, int k) {
    if (!pHead) return pHead;
    auto fast = pHead, slow = pHead;
    while (k--) {
        if (!fast) return nullptr; //链表长度小于k 返回空
        fast = fast->next;
    }
    while (fast) {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

 
9、复杂链表的复制
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针 random 指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)。 下图是一个含有5个结点的复杂链表。图中实线箭头表示 next 指针,虚线箭头表示 random 指针。为简单起见,指向 null 的指针没有画出。
在这里插入图片描述
思路1:使用哈希表, key = 源链表节点,value = 目标链表节点,遍历源链表,判断每个节点和 random 节点是否在 hash 表中,如果不存在则创建。
时间复杂度:O(n) ,空间复杂度:O(n)

RandomListNode* Clone(RandomListNode* pHead) {
        if(!pHead) return pHead;
        unordered_map<RandomListNode*, RandomListNode*> hash;
        hash[nullptr] = nullptr; //这如果不加会出现段错误 没理解
        auto dummyHead = new RandomListNode(-1),cur = dummyHead;
        while(pHead)
        {
            if(!hash.count(pHead)) hash[pHead] = new RandomListNode(pHead->label);
            if(!hash.count(pHead->random)) hash[pHead->random] = new RandomListNode(pHead->random->label);
            cur->next = hash[pHead];
            cur->next->random = hash[pHead->random];
            pHead = pHead->next;
            cur = cur->next;
        }
        return dummyHead->next;
    }

 
思路2:先把原链表每个节点后面拼接上复制链表的各个节点,然后再把两个链表拆分出来。
时间复杂度:O(n) ,空间复杂度:O(1)
(1)将原链表的结点后面连上对应的拷贝节点,最后链表变成 原1 -> 拷1 -> 原2 -> 拷2 -> … -> null 的形式
(2)接着复制原链表的随机指针,使用双指针,一个指针指向原链表的节点, 一个指向拷贝链表的节点,那么就有 拷->random = 原->random->next (random不为空)
(3)最后再将两条链表拆分即可

RandomListNode* Clone(RandomListNode* pHead) {
        if(!pHead) return nullptr;
        RandomListNode *currNode = pHead;
        //复制节点 原A->B->C   复制后为 A->A'->B->B'->C->C'
        while(currNode)
        {           
            RandomListNode *node = new RandomListNode(currNode->label);  //A'        
            node->next = currNode->next;// A' 指向 B
            currNode->next = node;// A 指向 A'
            currNode = node->next; // currNode 由 A 变成 B
        }       
        currNode = pHead;//开始复制旧的链表的随机指针 A’->random = A->random->next;
        while(currNode)
        {           
            if(currNode->random)
                currNode->next->random = currNode->random->next;
            currNode = currNode->next->next;//下一个旧链表的节点
        }       
        //拆分    将链表拆分为原链表和复制后的链表
        RandomListNode *pCloneHead = pHead->next; //A'
        currNode = pHead;
        while(currNode)
        {           
            RandomListNode *cloneNode = currNode->next;
            currNode->next = cloneNode->next;
            cloneNode->next = cloneNode->next == nullptr? nullptr:cloneNode->next->next;
            currNode =currNode->next ;       
        }       
        return pCloneHead;
    }

 
10、LRU算法(最近最少使用缓存机制)
实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间

举例
输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1);    缓存是 {1=1}
lRUCache.put(2, 2);    缓存是 {1=1, 2=2}
lRUCache.get(1);       返回 1
lRUCache.put(3, 3);   该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);      返回 -1 (未找到)
lRUCache.put(4, 4);     该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);     返回 -1 (未找到)
lRUCache.get(3);    返回 3
lRUCache.get(4);     返回 4

思路:哈希表 + 双向链表
LRU 是 Least Recently Used 的简写,是最近最少使用的意思。这个缓存器有两个成员函数,get 和 put,其中 get 是通过输入 key 来获得 value,如果成功获得后,这对 (key, value) 升至缓存器中最常用的位置(顶部),如果 key 不存在,则返回 -1。而 put 函数是插入一对新的 (key, value),如果原缓存器中有该 key,则需要先删除掉原有的,将新的插入到缓存器的顶部。如果不存在,则直接插入到顶部。若加入新的值后缓存器超过了容量,则需要删掉一个最不常用的值,即底部的数据。

具体实现时需要三个私有变量,cap, l 和 hash,其中 cap 是缓存器的容量,l 是保存缓存器内容的链表,hash 是 HashMap,保存关键值 key 和缓存器各项的迭代器之间映射,通过使用哈希表可以 O(1) 的时间内找到目标值。

get 和 put 如何实现 ?
get:在 HashMap 中查找给定的 key,若不存在直接返回 -1。如果存在则将此项移到顶部,我们可以使用 C++ STL 中 splice 函数将这个 key 对应的迭代器移动到链表的开头,然后返回 value即可。

put:先在 HashMap 中查找给定的 key,如果存在就删掉原有项,并在链表最前面插入新来项,然后判断是否超过了最大容量,若溢出则删掉链表最后面的项(即最不常用的数据)。

为什么使用 哈希表 + 双向链表 的数据结构?
双向链表插入和删除数据速度快,但是查找慢,因此借助哈希表可以提高查找速度,使得插入、查询和删除操作的时间复杂度为 O(1) 级别。

class LRUCache {
private:
    int cap;
    list<pair<int,int>> l;
    unordered_map<int,list<pair<int,int>>::iterator> hash;
public:
    LRUCache(int capacity) {
        cap = capacity;
    }
    
    int get(int key) {
       auto it = hash.find(key);  //返回指向该键值对的迭代器
       if(it == hash.end()) return -1; //没找到数据返回-1
       l.splice(l.begin(),l,it->second); //如果找到 需要把键值对移到最前面(最前面相当于是最近被访问过的)
       return it->second->second; //返回找到的数据
    }
    
    void put(int key, int value) {
        auto it = hash.find(key);
        if(it != hash.end()) l.erase(it->second); //如果数据存在,需要将原有项删除
        l.push_front(make_pair(key,value));   //重新在链表最前面插入新来项
        hash[key] = l.begin();   //更新哈希表
        
        //如果超过了最大容量需要删除链表最后面的一组数据同时更新哈希表
        if(hash.size() > cap) {
            auto k = l.rbegin()->first; //最后面一组数据的key
            l.pop_back();     //删除最后面一组数据
            hash.erase(k);    //删除哈希表中对应数据
        }
    }
};

 
 
 
大佬点个赞再走呗!
 
 
 
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西瓜味儿的小志

您的支持是创作的最大动力^_^

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值