掘金 人工智能 前天 19:23
【算法笔记】6.LeetCode-Hot100-链表专项
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了多道经典的链表算法题,涵盖了相交链表、反转链表、回文链表、环形链表、排序链表等常见场景。文章详细解析了每道题的解题思路,从朴素的暴力法到优化的双指针、快慢指针、哈希表等技巧,并提供了相应的C++代码实现。其中,对于回文链表和环形链表等问题,文章对比了不同方法的优劣,并指出了易错点。此外,还涉及了链表与数组的转换,以及归并排序在链表排序中的应用。整体内容旨在帮助读者理解和掌握链表相关的核心算法和优化策略。

💡 **链表相交与反转**:相交链表可通过双指针巧妙实现,遍历完一条链表后切换到另一条,实现长度对齐以找到交点。反转链表则需要额外指针暂存下一个节点,以避免链接丢失。

🧠 **回文链表检测**:回文链表可通过将链表复制后反转再比较,或将链表元素存入数组利用双指针判断,更优的方案是利用快慢指针找到中点,反转后半部分再进行比较。

⭕ **环形链表判断与定位**:判断链表是否存在环,可使用哈希表记录已访问节点,或采用快慢指针,两者相遇即表示存在环。定位环的入口节点,则需在快慢指针相遇后,将其中一个指针移至链表头部,两者同步前进,相遇处即为入口。

🔀 **链表操作的指针技巧**:在进行链表节点插入、删除或交换时,必须注意指针的指向,提前保存好后续节点的引用,避免链表断裂。例如,交换相邻节点时,需在修改节点指针前,确保其next指向已正确设置。

📊 **链表与数组的转换及应用**:在某些链表问题中,将链表转换为数组可以简化操作,如回文链表检测和两数相加,但需注意数组大小限制。排序链表也可通过转为数组排序再重建链表实现,但归并排序是更符合链表特性的最优解。

➕ **复杂链表操作的策略**:如合并k个有序链表,可采用两两合并的策略;而随机链表的复制则需借助哈希表来映射原节点与新节点,并处理random指针的指向;LRU缓存则结合哈希表和双向链表,实现高效的查找与淘汰。

本文开始,不再记录原题,只记录解题思路和 AC 代码,加快进度。

1. 相交链表(t160)

思路分析:一上来没看懂这题要做什么,看了题解才知道这题是要找到两个链表相交的首个公共节点,一个巧妙的解法就是用双指针,一个指针遍历完之后,指向另一条链表的头节点,这样相当于做了一个长度对齐。

图源:leetcode.cn/problems/in…

/** * Definition for singly-linked list. * struct ListNode { *     int val; *     ListNode *next; *     ListNode(int x) : val(x), next(NULL) {} * }; */class Solution {public:    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {        if (headA == nullptr || headB == nullptr) {            return nullptr;        }        ListNode *pA = headA, *pB = headB;        while (pA != pB) {            pA = pA == nullptr ? headB : pA->next;            pB = pB == nullptr ? headA : pB->next;        }        return pA;    }};

2. 反转链表(t206)

思路分析:题目比较简单,但需要注意用额外的 next 指针去存储原本的下一个节点,因为 head-> next之后,原有的链接关系会丢失。

/** * Definition for singly-linked list. * struct ListNode { *     int val; *     ListNode *next; *     ListNode() : val(0), next(nullptr) {} *     ListNode(int x) : val(x), next(nullptr) {} *     ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */class Solution {public:    ListNode* reverseList(ListNode* head) {        ListNode *prev = nullptr;        ListNode *next;        while (head){               next = head->next;            head->next = prev;            prev = head;            head = next;        }        return prev;    }};

3. 回文链表(t234)

思路分析:回文链表是指向前和向后读都相同的链表,刚做完上题的反转链表,一个朴素的思想就是把链表先翻转一下,然后两个链表遍历对比,于是写了以下代码:

class Solution {public:    bool isPalindrome(ListNode* head) {        ListNode *curr = head;        ListNode *prev = nullptr;        ListNode *next;        while (curr){            next = curr->next;            curr->next = prev;            prev = curr;            curr = next;                    }        while (head){            if (prev->val != head -> val) return false;            prev = prev -> next;            head = head -> next;        }        return true;    }};

上面这个代码无法通过,因为犯了一个错误,*curr = head 并没有复制链表,而是使 curr 和 head 指向了同一个链表(地址相同)。因此,要复制链表,需要单独 new 一个 ListNode,再遍历复制。

ListNode* cloneList(ListNode* head) {    if (!head) return nullptr;    ListNode* newHead = new ListNode(head->val);    ListNode* tail = newHead;    head = head->next;    while (head) {        tail->next = new ListNode(head->val);        tail = tail->next;        head = head->next;    }    return newHead;}class Solution {public:    bool isPalindrome(ListNode* head) {        ListNode* copy = cloneList(head)        // 原地反转 copy        ListNode *curr = copy;        ListNode *prev = nullptr;        ListNode *next;        while (curr){            next = curr->next;            curr->next = prev;            prev = curr;            curr = next;                    }        // 和 head 比较        while (head){            if (prev->val != head->val) return false;            prev = prev->next;            head = head->next;        }        return true;    }};

这样做拷贝显然过于复杂,一个更好的思路是将链表转换成数组,因为数组更方便查询。转换成数组后,就可以通过双指针的思路,往中间靠近查询。

class Solution {public:    bool isPalindrome(ListNode* head) {        vector<int> vals;        ListNode* curr = head;        while (curr) {            vals.push_back(curr->val);  // 拷贝值            curr = curr->next;        }        int left0, right = vals.size() - 1;        while (left < right) {            if (vals[left++] != vals[right--]) return false;        }        return true;    }};

如果不用数组,有没有更好的思路呢?看了题解,可以用快慢指针+链表翻转的思路去做。所谓快慢指针,就是用两个指针,快指针一次走两步,慢指针一次走一步,这样当快指针到结尾时,慢指针正好到中心。之后,把后半链表翻转,再逐位比对就行了。

class Solution {public:    bool isPalindrome(ListNode* head) {        ListNode* fast = head, *slow = head;        while (fast && fast->next)        {            slow = slow->next;            fast = fast->next->next;        }        // 翻转后半部分        ListNode* prev = nullptr;        while (slow)        {            ListNode* next = slow->next;            slow->next = prev;            prev = slow;            slow = next;        }        // 比较是否回文        ListNode* cur1 = head, *cur2 = prev;        while (cur1 && cur2)        {            if (cur1->val != cur2->val) return false;            cur1 = cur1->next;            cur2 = cur2->next;        }        return true;    }};

4. 环形链表(t141)

思路分析:环形链表的意思就是链表的末尾会链接到已出现过的节点中,形成“环状”循环。判断环形链表的标准就是看是否已经有节点地址出现过,既然是判断重复问题,第一想到的就是哈希表。

class Solution {public:    bool hasCycle(ListNode *head) {        unordered_set <ListNode*> listset;        while (head){            listset.insert(head);            head = head->next;            if (listset.count(head)) return true;        }        return false;    }};

看题解还有另外一种用快慢指针的巧妙思路,如果两个人在操场跑步,一个人跑得快,一个人跑得慢,两个人势必会相遇。反言之,如果两个人相遇,则证明跑道是圆的,思路很巧妙。

class Solution {public:    bool hasCycle(ListNode *head) {        if (head == nullptr || head->next == nullptr) {            return false;        }        ListNode *slow = head, *fast = head;        while (slow != fast){            if (fast == nullptr || fast->next == nullptr) {                return false;            }            slow = slow->next;            fast = fast->next->next;        }        return true;    }};

5. 环形链表 II(t142)

思路分析:这道题和上一道换汤不换药,无非是返回值更加精确,需要返回入环的第一个节点,如果用哈希表去求解,代码几乎不用变,把返回值改一下就行了。

class Solution {public:    ListNode *detectCycle(ListNode *head) {        unordered_set <ListNode*> list_set;        while (head){            if (list_set.count(head)) return head;            list_set.insert(head);            head = head->next;        }        return nullptr;    }};

用快慢指针求解反而会更加“烧脑”,需要经历一些数学推导:

假设:链表起点到入环口距离为 a;环入口到相遇点距离为 b;环的长度为 c。

当 fast 和 slow 第一次相遇时,有:

fast 走的路程 = a + b + n * cslow 走的路程 = a + bfast = 2 * slow=> a + b + n * c = 2(a + b)=> a = n * c - b

得出结论:如果从起点走 a 步,从相遇点也走 a 步,它们就会在入环口相遇!

根据这个结论,具体方法就是当 fast 和 slow 相遇后,让 fast 从头一步步慢走,两人就会在入口相遇。

class Solution {public:    ListNode *detectCycle(ListNode *head) {        ListNode* fast = head;        ListNode* slow = head;        while (true) {            if (fast == nullptr || fast->next == nullptr) return nullptr;            fast = fast->next->next;            slow = slow->next;            if (fast == slow) break;        }        fast = head;        while (slow != fast) {            slow = slow->next;            fast = fast->next;        }        return fast;    }};

6. 合并两个有序链表(t21)

思路分析:这一题的关键是要new一个哨兵节点 prehead,然后用一个指针去帮它“铺路”。

class Solution {public:    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {        ListNode* preHead = new ListNode(-1);        ListNode* prev = preHead;                while (l1 != nullptr && l2 != nullptr){            if (l1->val > l2->val){                prev->next = l2;                l2 = l2->next;            }            else{                prev->next = l1;                l1 = l1->next;            }            prev = prev->next;        }        prev->next = (l1 == nullptr? l2 : l1);        return preHead->next;    }};

7. 两数相加(t2)

思路分析:这题要处理两个不等长的逆序链表相加,由于链表无法直接知道长度,因此一个朴素的思路就是直接将其转成数组,这样运算就稍微简单一点,最后算完再转回链表。

class Solution {public:    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {        vector<int> l1_list, l2_list;        ListNode* curr1 = l1;        ListNode* curr2 = l2;                while (curr1){            l1_list.push_back(curr1->val);            curr1 = curr1->next;        }        while (curr2){            l2_list.push_back(curr2->val);            curr2 = curr2->next;        }        int sum0, sum1 = 0, sum2 = 0;        int n1 = l1_list.size(), n2 = l2_list.size();        for (int i = n1 - 1 ; i >= 0; i--){            sum1 += l1_list[i] * pow(10, i);        }        for (int i = n2 - 1 ; i >= 0; i--){            sum2 += l2_list[i] * pow(10, i);        }        sum = sum1 + sum2;                ListNode* dummy = new ListNode(-1);        ListNode* curr = dummy;        if (sum == 0) return new ListNode(0);        while (sum > 0) {            int digit = sum % 10;            curr->next = new ListNode(digit);            curr = curr->next;            sum /= 10;        }        return dummy->next;    }};

这样写,测试样例能过,但没法AC,因为犯了一个“致命”错误,那就是int类型的数组是无法存储超大整数的。

因此,正确的方式是直接用链表去逐位处理,关键问题是如何处理进位问题,可以用一个单独的 carry 变量进行存储。

class Solution {public:    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {        ListNode* dummy = new ListNode(-1);        ListNode* curr = dummy;        int carry0;        while (l1 || l2 || carry) {            int x = l1 ? l1->val : 0;            int y = l2 ? l2->val : 0;            int sum = x + y + carry;            carry = sum / 10;            curr->next = new ListNode(sum % 10);            curr = curr->next;            if (l1) l1 = l1->next;            if (l2) l2 = l2->next;        }        return dummy->next;    }};

8. 删除链表的倒数第 N 个结点(t2)

思路分析:这道题要求删除倒数第 N 个节点,顺序遍历时,怎么知道在哪开始删除呢?一个最直接的思路就是先把链表遍历一遍,先得到链表的长度。

class Solution {public:    ListNode* removeNthFromEnd(ListNode* head, int n) {        ListNode* dummy = new ListNode(0, head);        ListNode* prev = dummy;        int length0;        while (prev){            prev = prev -> next;            length++;        }        ListNode* curr = dummy;                int position0;        while (position < length - n - 1){            curr = curr->next;            position++;        }                // 删除节点        curr->next = curr->next->next;        return dummy->next    }};

看题解还可以采用双指针的思路,既然不知道 n 具体在哪里,那就让其中一个指针先抢跑 n 个位置,这样当它到末尾时,慢的指针刚好到n。

class Solution {public:    ListNode* removeNthFromEnd(ListNode* head, int n) {        ListNode* dummy = new ListNode(0, head);        ListNode* first = head;        ListNode* second = dummy;        for (int i0; i < n; ++i) {            first = first->next;        }        while (first) {            first = first->next;            second = second->next;        }        second->next = second->next->next;        ListNode* ans = dummy->next;        delete dummy;        return ans;    }};

9. 两两交换链表中的节点(t24)

思路分析:这道题可以直接用迭代思想去做,操作比较常规,唯一需要注意的是,在进行结点替换时,一定要先把节点后路留好,比如下面的代码中在进行node2->next = node1;之前,必须先把node1的next定好,否则链就丢了,找不到了。

class Solution {public:    ListNode* swapPairs(ListNode* head) {        ListNode* dummy = new ListNode(0);        ListNode* curr = dummy;        curr->next = head;        while (curr->next != nullptr && curr->next->next != nullptr){            ListNode* node1 = curr->next;            ListNode* node2 = curr->next->next;            curr->next = node2;            node1->next = node2->next;            node2->next = node1;            curr = node1;        }        return dummy->next;    }};

10. K 个一组翻转链表(t25)

思路分析:这道题是hard难度,需要单独对每块内容进行链表翻转,最容易理解的写法就是单独弄个函数处理链表翻转,返回头和尾,在外面将其连接。

class Solution {public:    // 翻转一个子链表,并且返回新的头与尾    pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {        ListNode* prev = tail->next;        ListNode* p = head;        while (prev != tail) {            ListNode* nex = p->next;            p->next = prev;            prev = p;            p = nex;        }        return {tail, head};    }    ListNode* reverseKGroup(ListNode* head, int k) {        ListNode* hair = new ListNode(0);        hair->next = head;        ListNode* pre = hair;        while (head) {            ListNode* tail = pre;            // 查看剩余部分长度是否大于等于 k            for (int i0; i < k; ++i) {                tail = tail->next;                if (!tail) {                    return hair->next;                }            }            ListNode* nex = tail->next;            tie(head, tail) = myReverse(head, tail);            // 把子链表重新接回原链表            pre->next = head;            tail->next = nex;            pre = tail;            head = tail->next;        }        return hair->next;    }};

11. 随机链表的复制(t138)

思路分析:这道题看了题目和示例也没看懂要做什么,看了题解才知道原来要做的就是对特殊格式的链表进行深拷贝。

比如,直接用python3调用深拷贝的工具

class Solution:    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':        return copy.deepcopy(head)

可以通过哈希表的方式具体实现:

class Solution {public:    Node* copyRandomList(Node* head) {        if(head == nullptr) return nullptr;        Node* cur = head;        unordered_map<Node*, Node*> map;        // 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射        while(cur != nullptr) {            map[cur] = new Node(cur->val);            cur = cur->next;        }        cur = head;        // 构建新链表的 next 和 random 指向        while(cur != nullptr) {            map[cur]->next = map[cur->next];            map[cur]->random = map[cur->random];            cur = cur->next;        }        // 返回新链表的头节点        return map[head];    }};

12. 排序链表(t148)

思路分析:数组的排序很简单,有stl提供的sort方式可以直接调用,链表排序没这样方便的函数,因此,一个简单的想法就是将链表转成数组,排完序之后再转成链表。

class Solution {public:    ListNode* sortList(ListNode* head) {        ListNode* curr = head;        vector<int> nums;        while (curr){            nums.push_back(curr->val);            curr = curr->next;        }                sort(nums.begin(), nums.end());                ListNode* dummy = new ListNode();        ListNode* cur = dummy;        for (int i0; i < nums.size(); i++){            cur->next = new ListNode(nums[i]);            cur = cur->next        }                return dummy->next;    }};

虽然这样写能够AC,但显然不是题目的本意,看官方题解,推荐采用归并排序去做,归并排序就是“分治”思想:

class Solution {public:    ListNode* sortList(ListNode* head) {        return sortList(head, nullptr);    }    ListNode* sortList(ListNode* head, ListNode* tail) {        if (head == nullptr) {            return head;        }        if (head->next == tail) {            head->next = nullptr;            return head;        }        ListNode* slow = head, *fast = head;        while (fast != tail) {            slow = slow->next;            fast = fast->next;            if (fast != tail) {                fast = fast->next;            }        }        ListNode* mid = slow;        return merge(sortList(head, mid), sortList(mid, tail));    }    ListNode* merge(ListNode* head1, ListNode* head2) {        ListNode* dummyHead = new ListNode(0);        ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;        while (temp1 != nullptr && temp2 != nullptr) {            if (temp1->val <= temp2->val) {                temp->next = temp1;                temp1 = temp1->next;            } else {                temp->next = temp2;                temp2 = temp2->next;            }            temp = temp->next;        }        if (temp1 != nullptr) {            temp->next = temp1;        } else if (temp2 != nullptr) {            temp->next = temp2;        }        return dummyHead->next;    }};

13. 合并 K 个升序链表(t23)

思路分析:第六题做过合并两个有序链表,因此,最简单的思路就是直接两两合并。虽然这不是最优思路,但对于 hard 题来说,能解决就行,没必要为最优方法浪费太多时间。

class Solution {public:    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {        if ((!a) || (!b)) return a ? a : b;        ListNode head, *tail = &head, *aPtr = a, *bPtr = b;        while (aPtr && bPtr) {            if (aPtr->val < bPtr->val) {                tail->next = aPtr; aPtr = aPtr->next;            } else {                tail->next = bPtr; bPtr = bPtr->next;            }            tail = tail->next;        }        tail->next = (aPtr ? aPtr : bPtr);        return head.next;    }    ListNode* mergeKLists(vector<ListNode*>& lists) {        ListNode *ans = nullptr;        for (size_t i0; i < lists.size(); ++i) {            ans = mergeTwoLists(ans, lists[i]);        }        return ans;    }};

14. LRU 缓存(t146)

思路分析:这道题有点偏具体的场景,看题目就没看懂,所谓 LRU,全称是 Least Recently Used,意思是:如果缓存满了,要优先淘汰最久没被使用的那一项。

比如,在书架上放书:

LRUCache 的需求如下:

因此,这道题主要需要解决的两个难点就是查找结点是否存在和插入删除或移动节点,前者可以用哈希表解决,后者可以用双向链表解决。第一次刷到此题不妨先跳过,等后面功力提升之后再回头过来看。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

链表 算法 数据结构 C++ 编程技巧
相关文章