【LeetCode 热题 100】链表 系列

📁206. 反转链表

对于每一个节点cur,都是将next节点指向cur,cur节点指向上一个节点head。因此可以采用递归的策略,从后往前进行上述操作,期间记录最后一个节点并返回。

我是将递归分为3类:

  1. 前序递归:在递归之前进行处理。

  2. 中序递归:在递归中进行处理。

  3. 后续递归:在递归后处理。

本地就是一个后续递归的操作,地递归到最后一层,从后往前进行处理。

cpp 复制代码
ListNode* reverseList(ListNode* head) {
        if(head == nullptr || head->next == nullptr)
            return head;

        ListNode* newHead = reverseList(head->next);

        ListNode* next = head->next;
        head->next = next->next;
        next->next = head;

        return newHead;
    }

📁160. 相交链表

我们假设A链表总结点个数是a,B链表总结点个数是b,如果存在公共节点node,那么我们假设公共节点后面所有节点个数是c。

指针 cur1 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:

a+(b−c)

指针 cur2 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:

b+(a−c)

那么可以列式,并有两种情况:

a+(b−c)=b+(a−c)

1. 如果两链表有公共节点(c > 0):节点 cur1 和 cur2 指向公共节点node

2. 如果不存在公共节点(c = 0):节点 cur1 和 cur2 都指向nullptr

cpp 复制代码
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* cur1 = headA , *cur2 = headB;
        while(cur1 != cur2)
        {
            cur1 = cur1 != nullptr ? cur1->next : headB;
            cur2 = cur2 != nullptr ? cur2->next : headA;
        }
        return cur1;
    }

📁234. 回文链表

一个简单的思路就是取出链表中的元素,放到数组中,使用双指针判断数组是否是回文。

cpp 复制代码
bool isPalindrome(ListNode* head) {
        vector<int> ret;
        while(head)
        {
            ret.push_back(head->val);
            head = head->next;
        }

        int right = ret.size() - 1 , left = 0;
        while(left < right)
        {
            if(ret[left++] != ret[right--])
                return false;
        }

        return true;
    }

📁141. 环形链表

我们来假设一种情况,在一条无限长的跑道上,小明每秒走x步,小妹每秒走x+1步,并且小美在小明后面追赶,如果情况成立小美一定能追上小明,因为他们之间的距离每次都1。

对于该题目,采用双指针思路,如果链表有环,那么slow指针进入换后就一定能被fast指针追上。

cpp 复制代码
bool hasCycle(ListNode *head) {

        ListNode* fast = head;
        ListNode* slow = head;

        while(fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast)
                return true;
        }

        return false;
    }

📁142. 环形链表 II

符号定义与初始条件​

  • ​头节点​head):链表的起点。
  • ​环入口节点​A):链表开始形成环的节点。
  • ​相遇点​B):快慢指针第一次相遇的位置。
  • ​a​:头节点到环入口节点的距离。
  • ​b​:环入口节点到相遇点的距离。
  • ​c​:相遇点到环入口节点的剩余距离。
  • ​环周长​n = b + c(环的总长度)。
  • ​快指针速度​:每次移动 2 步。
  • ​慢指针速度​:每次移动 1 步。

Sfast​=2⋅Sslow​⟹a+k(b+c)+b=2(a+b)

将上述方程化简:

a+k(b+c)+b=2a+2b⟹k(b+c)=a+b**⟹ a=(k−1)(b+c)+c**

  • 等于 ​c​ 加上 (k - 1) 圈环的周长。
  • k = 1,则 a = c,即从头节点到环入口的距离等于相遇点到环入口的距离。
cpp 复制代码
ListNode *detectCycle(ListNode *head) {
        
        ListNode* fast = head;
        ListNode* slow = head;

        while(fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast)
            {
                while(slow != head)
                {
                    slow = slow->next;
                    head = head->next;
                }
                return head;
            }
        }
        return nullptr;
    }

📁2. 两数相加

本题就是一道类似高精度的问题,但是本题相较简单,我们只需要从后往前遍历即可,每次取出两个个位数相加,对结果进行模除操作结果放到新节点中即可。

此外,需要注意的是,最后需要判断最后一位数相加是否还需要进位。

cpp 复制代码
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* newHead = new ListNode();
        ListNode* tail = newHead;
        int tmp = 0;
        while(l1 != nullptr || l2 != nullptr)
        {
            if(l1)
            {
                tmp += l1->val;
                l1 = l1->next;
            }
            if(l2)
            {
                tmp += l2->val;
                l2 = l2->next;
            }
            tail->next = new ListNode(tmp % 10);
            tmp /= 10;
            tail = tail->next;
        }

        if(tmp)
            tail->next = new ListNode(tmp);

        tail = newHead->next;
        delete newHead;
        return tail;
    }

📁 21. 合并两个有序链表

最简单的一个思路就是创建一个新的链表,然后遍历两个链表,将较小值的节点尾插到新节点中。

cpp 复制代码
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* cur1 = list1 , *cur2 = list2;
        ListNode* newHead = new ListNode();
        ListNode* tail = newHead;

        while(cur1 && cur2)
        {
            if(cur1->val < cur2->val)
            {
                tail->next = cur1;
                tail = tail->next;
                cur1 = cur1->next;
            }
            else
            {
                tail->next = cur2;
                tail = tail->next;
                cur2 = cur2->next;
            }
        }

        while(cur1)
        {
            tail->next = cur1;
            tail = tail->next;
            cur1 = cur1->next;
        }

        while(cur2)
        {
            tail->next = cur2;
            tail = tail->next;
            cur2 = cur2->next;
        }

        tail = newHead->next;
        delete newHead;
        return tail;
    }

也可以采用递归的方法。

  1. 递归函数的返回值:返回合并后链表的头结点。

  2. 递归函数结束条件:如果有一个链接节点为空,返回另一个节点。

  3. 递归过程:比较两个链表的节点,取出较小值作为当前轮次的头结点,它的next指向递归的返回值,即两个链表剩下节点合并后链表的头结点。

cpp 复制代码
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if(!list1)  return list2;
        if(!list2)  return list1;

        if(list1->val < list2->val)
            list1->next = mergeTwoLists(list1->next , list2);
        else 
            list2->next = mergeTwoLists(list1 , list2->next);

        return list1->val < list2->val ? list1 : list2;
    }

📁 19. 删除链表的倒数第 N 个结点

从头节点开始对链表进行一次遍历,得到链表的长度 L。随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L− n + 1 个节点时,它就是我们需要删除的节点。

cpp 复制代码
ListNode* removeNthFromEnd(ListNode* head, int n) {
        int sz = 0;
        ListNode* cur = head;
        while(cur)
        {
            ++sz;
            cur = cur->next;
        }
        
        //创建新的头结点方便处理只有一个节点的特殊情况
        ListNode* newhead = new ListNode( 0 , head);
        cur = newhead;

        for(int i = 0 ; i < sz - n; ++i)
        {
            cur = cur->next;
        }
        cur->next = cur->next->next;

        return newhead->next;
    }

📁 24. 两两交换链表中的节点

本题我们可以看成反转链表的一种变种,只不过是变成两两交换了,第一个节点head , 第二个节点next,head节点next指向剩余链表中交换后的链表的头结点,next节点的next指向head节点。

cpp 复制代码
ListNode* swapPairs(ListNode* head) {
        if(!head || !head->next)
            return head;
        
        ListNode* next = head->next;
        head->next = swapPairs(next->next);
        next->next = head;

        return next;
    }   

📁 25. K 个一组翻转链表

就是将 n / k 个组内的节点进行头插,然后将剩下的节点在链入链表中即可。

cpp 复制代码
ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* newHead = new ListNode();
        ListNode* tail = newHead;    //组内最后一个节点

        ListNode* cur = head;
        int n = 0;
        while(cur)
        {
            ++n , cur = cur->next;
        }
        n /= k;

        cur = head;
        for(int i = 0 ; i < n ; ++i)
        {
            //每组第一个节点经过翻转后变成最后一节点 
            ListNode* tmp = cur;
            for(int j = 0 ; j < k ; ++j)
            {   
                ListNode* next = cur->next;
                
                cur->next = tail->next;
                tail->next = cur;

                cur = next;
            }
            tail = tmp;
        }

        if(cur)
            tail->next = cur;

        return newHead->next;
    }

📁 138. 随机链表的复制

对于本题最苦难的地方就在于随机指针的指向问题了,因为random可能指向一个还没有创建的节点node,如果我们直接创建,并使得random指向node,那么node之前的节点的next指针怎么指向node,因此我们需要一个东西来记录下来。

我们可以使用unordered_map来记录原节点和新节点的对应关系,这样random指向一个没有创建的节点node时,node可以直接创建,并且node节点之前的节点prev的next指针可以通过hash找到node节点,并成功指向。

cpp 复制代码
class Solution {
public:
    unordered_map<Node* , Node*> hash;

    Node* copyRandomList(Node* head) {
        if(head == nullptr)
            return head;

        if(!hash.count(head))
        {
            Node* newNode = new Node(head->val);

            hash[head] = newNode;

            newNode->next = copyRandomList(head->next);
            newNode->random = copyRandomList(head->random);
        }

        return hash[head];
    }
};

📁 148. 排序链表

最简单的方法就是节点排序,然后创建新的链表,按值大小从后往前链接节点。我们使用map记录值和节点直接的映射关系,因为底层是红黑树,即平衡二叉搜索树,所以遍历时是从小到大遍历的,multimap可以保证我们存在相同值。

cpp 复制代码
ListNode* sortList(ListNode* head) {
        if(head == nullptr || head->next == nullptr)
            return head;

        multimap<int , ListNode*> hash;
        ListNode* cur = head;
        while(cur)
        {   
            hash.insert(make_pair(cur->val , cur));
            cur = cur->next;
        }

        ListNode* newHead = new ListNode();
        ListNode* tail = newHead;

        for(auto& [key , node] : hash)
        {
            tail->next = node;
            tail = tail->next;
        }
        tail->next = nullptr;
        

        return  newHead->next;
    }

📁 23. 合并 K 个升序链表

对于链表,我可以将翻转看成将每个节点从后往前进行头插。那么本题就简单了,我们只需要头插节点即可。

"如果节点总数不是 k的整数倍,那么请将最后剩余的节点保持原有顺序",可知我们需要翻转组数目 = n / k。

此外,进行头插后,我们还需要记录下来每组第一个节点,每组进行反转后第一个节点变为了最后一个节点,它的next节点应该指向下一组翻转节点。

cpp 复制代码
struct cmp
    {
        bool operator()(const ListNode* l1 , const ListNode* l2)
        {
            return l1->val > l2->val;
        }
    };
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        std::priority_queue<ListNode* , std::vector<ListNode*> , cmp> heap;
        for(ListNode* node : lists)
            if(node != nullptr)
                heap.push(node);

        ListNode* newHead = new ListNode();
        ListNode* tail = newHead;

        while(!heap.empty())
        {
            ListNode* node = heap.top(); heap.pop();
            tail->next = node;
            tail = tail->next;
            if(node->next != nullptr)
                heap.push(node->next);
        }

        return newHead->next; 
    }

📁 146. LRU 缓存

本题就是将将新插入或刚被调用的元素插入到链表头部,当容量满时淘汰掉链表尾部元素,即最近最少被使用的元素。

为什么使用unorder_map,因为哈希系列查找速度是O(1),而链表的查找速度是O(N),为了提高效率我们使用哈希来查找存储 <key,val>元素的链表节点的iterator。

cpp 复制代码
class LRUCache {
public:
    typedef list<pair<int,int>>::iterator it;
    LRUCache(int capacity) {
        _capacity = capacity;
    }

    //判断是否缓存
    //找到了 将其剪切到链表的头部位置
    int get(int key) {
        auto kv = _hash.find(key);
        if(kv != _hash.end())
        {
            //push过 已被缓存
            _list.splice(_list.begin() , _list , kv->second);
            return kv->second->second;
        }
        return -1;
    }

    //1. 首先查找LRU中是否缓存了
    //2. 如果存在, 更新val 并且放到链表的最前方
    //3. 如果不存在 在判断链表是否未满
        //i. 如果链表满了 尾删
        //ii. 其次 头插并记录映射
    void put(int key, int value) {
        auto kv = _hash.find(key);
        if(kv != _hash.end())
        {
            //更新
            kv->second->second = value;
            _list.splice(_list.begin() , _list , kv->second);
        }
        else
        {
            if(_list.size() == _capacity)
            {
                //尾删一个元素
                _hash.erase(_list.back().first);
                _list.pop_back();
            }
            //头插新元素
            _list.emplace_front(key , value); 
            _hash[key] = _list.begin();
        }
    }

private:
    int _capacity = 0;
    unordered_map<int , it> _hash;
    list<pair<int , int>> _list;
};
相关推荐
酷ku的森2 小时前
数据结构:链表
数据结构·链表
何其有幸.3 小时前
实验3-3 比较大小(PTA|C语言)
c语言·数据结构·算法
东阳马生架构4 小时前
Sentinel源码—8.限流算法和设计模式总结二
算法·设计模式·sentinel
老饼讲解-BP神经网络5 小时前
一篇入门之-评分卡变量分箱(卡方分箱、决策树分箱、KS分箱等)实操例子
算法·决策树·机器学习
何其有幸.5 小时前
实验6-3 使用函数求特殊a串数列和(PTA|C语言)
c语言·数据结构·算法
不会计算机的捞地5 小时前
【数据结构入门训练DAY-24】美国大选
数据结构·算法
明月看潮生6 小时前
青少年编程与数学 02-018 C++数据结构与算法 11课题、分治
c++·算法·青少年编程·编程与数学
Echo``6 小时前
2:QT联合HALCON编程—图像显示放大缩小
开发语言·c++·图像处理·qt·算法
.似水6 小时前
2025.4.22_C_可变参数列表
java·c语言·算法
Felven7 小时前
A. Ideal Generator
java·数据结构·算法