【算法笔记】6.LeetCode-Hot100-链表专项
本文开始,不再记录原题,只记录解题思路和 AC 代码,加快进度。
1. 相交链表(t160)
思路分析:一上来没看懂这题要做什么,看了题解才知道这题是要找到两个链表相交的首个公共节点,一个巧妙的解法就是用双指针,一个指针遍历完之后,指向另一条链表的头节点,这样相当于做了一个长度对齐。
/*** 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); // 原地反转 copyListNode *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 left = 0, 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 * c
slow 走的路程 = a + b
fast = 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 sum = 0, 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 carry = 0;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 length = 0;while (prev){prev = prev -> next;length++;}ListNode* curr = dummy;int position = 0;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 i = 0; 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;// 查看剩余部分长度是否大于等于 kfor (int i = 0; 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 i = 0; 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 i = 0; i < lists.size(); ++i) {ans = mergeTwoLists(ans, lists[i]);}return ans;}
};
14. LRU 缓存(t146)
思路分析:这道题有点偏具体的场景,看题目就没看懂,所谓 LRU,全称是 Least Recently Used,意思是:如果缓存满了,要优先淘汰最久没被使用的那一项。
比如,在书架上放书:
- 拿过的书放在最上面(最近使用);
- 最久没动过的书在最底下(最老);
- 书架满了,就把最底下那本书丢掉,腾地方给新书。
LRUCache 的需求如下:
- get(key):如果 key 在缓存中,返回它的值;并且把它放到最前面(表示最近使用过);
- put(key, value):放一对 key-value,如果 key 已经存在,更新值并放到前面;否则新增一个;如果满了,删除最久没用过的那一项(链表尾部)
因此,这道题主要需要解决的两个难点就是查找结点是否存在和插入删除或移动节点,前者可以用哈希表解决,后者可以用双向链表解决。第一次刷到此题不妨先跳过,等后面功力提升之后再回头过来看。