花2天把Leetcode上Linked lists的題目刷完了,下面是一些我覺得比較有傾訴欲望的題。
237. Delete Node in a Linked List
Write a function to delete a node (except the tail) in a singly linked list, given only access to that node. Supposed the linked list is 1 -> 2 -> 3 -> 4 and you are given the third node with value 3, the linked list should become 1 -> 2 -> 4 after calling your function.
這道題我的第一反應是,it's impossible!只給了這個node,鬼知道它前面那個node是啥呀,這不是怎么都找不到的么。后來看了答案覺得真是骨骼清奇,它采取的并不是尋常的找到前一個node,改變pointer這樣的方式,而是很tricky地,先將這個node的值改成下一個node的值,然后刪掉下一個node。不過話說回來,這道題我覺得并不是很合理,嚴格來說這不算刪掉了這個node,它刪掉的是下一個node,不太嚴謹。不過當做一種思路的擴充吧。
class Solution {
public:
void deleteNode(ListNode* node)
{
node->val = node->next->val;
ListNode* next = node->next->next;
delete node->next;
node->next = next;
}
};
206. Reverse Linked List
Reverse a singly linked list.
Hint:A linked list can be reversed either iteratively or recursively. Could you implement both?
這題我覺得很好,用兩種方式來翻轉鏈表,越基礎越是需要掌握。
Iteratively的做法,如果不借助其他pointer的幫助,我們只能每次都從頭找到尾,太笨了。如果設置一個prev pointer,那么對cur node來說,前后關節打通了,實際上成為了一個雙向鏈表,操縱一下pointer就可以實現翻轉了,不是什么難事。這里的思想方法其實就是把單向鏈表轉化為雙向鏈表來做。
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;
}
Recursively的做法,比前者稍微難一點。這個方法我并沒有自己想出來,我的思路是:要翻轉這個鏈表,肯定要先翻轉cur->next這個鏈表……這么下去到最后一個鏈表返回的就是最后一個node。我們需要實現:return node->next = head,但是最后我們需要返回的結果卻是最后一個node,這就要求我們能return兩個值,無法實現。所以我就卡住了。
這種思路其實也挺正常的,但是當思路不對的時候,應該再想想有沒有其它思路——這點也許是我欠缺的。答案中,我們return node就是最后一個node,那么難點就在于如何將head和后面一個已經反轉的鏈表聯系起來。其實稍微想一想就得出,head->next->next = head就可以實現目標了。
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
if (!head || !head->next) return head;
ListNode* last = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return last;
}
};
其實再仔細一想,作為一個recursion,寫的時候其實可以把結構都先描畫出來,像列提綱一樣,就是把recurse()和return x;寫好在那邊,那目標就很明確,我們在利用recursion解決什么subproblem,我們最后要返回的是什么。我們最后要返回的是什么,這點在recursion中至關重要,卻也最容易迷失。recursion的問題其實就是假設這個recursion works,subproblem已經解決了,對于解決這個整個問題有什么用處,recursion就像一個api一樣,是讓我們去調用的,最終在于返回一個結果。當我們將最后我們要返回什么搞清楚,很多時候問題就清楚了。
141. Linked List Cycle
Given a linked list, determine if it has a cycle in it.
Follow up:
Can you solve it without using extra space?
這道題利用了Floyd's hare and tortoise algorithm來找到cycle。以下的證明供我自己容易理解、記住而得:
算法:已知一個鏈表存在循環,用兩個指針,一快一慢,快的速度是慢的兩倍,從頭開始iterate這個鏈表,那么它們一定會在某一個node相遇。
證明:最intuitive的思想:當慢指針剛剛進入循環,快指針已經進入循環中的某一個節點了,因為快慢指針相對速度差一個節點,慢指針進步一格,快指針進步兩格,這相當于說慢指針不動,快指針進步一格,這樣快指針肯定能追上慢指針,它們一定會相遇。
算法:將快指針放到鏈表開頭,慢指針依舊在兩者相遇處,每次兩格指針同時前進一格,第二次它們相遇的節點,就是循環的開始。
證明:令循環長度為n,進入循環前的長度為x,循環開始到相遇地距離為y,相遇地到循環開始距離為z。到相遇時,慢指針走過的距離是d=x+y,快指針走過的距離是D=x+y+kn。假如k>1,那么說明快指針
與#142題相結合,以下是檢測有無cycle以及返回cycle開始的節點的代碼:
class Solution {
public:
ListNode *detectCycle(ListNode *head)
{
ListNode* copy = head, *slow = head, *fast = head;
while (fast && fast->next && fast->next->next)
{
slow = slow->next;
fast = fast->next->next;
if (fast == slow) break;
}
if (!fast || !fast->next || !fast->next->next) return nullptr;
slow = head;
while (fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return fast;
}
};
時間復雜度為O(N+K),其中N是循環起點之前的node數,K是循環node數。因為在循環中轉了一圈之后,快指針肯定能追上慢指針了??臻g復雜度為O(1)。
160. Intersection of Two Linked Lists
Write a program to find the node at which the intersection of two singly linked lists begins.
這道題我有幾種思路:
1、延續前面討論過的龜兔賽跑算法,完全可以找出這個node,也就是循環開始的這個node。當然,用這種方法要先構造出一個環路,最后把結構還原就行了。
2、可以通過判斷最后一個節點是否一樣來判斷兩個鏈表有無交集。如果有交集,可以通過計算長度來判斷交集點。因為從交集開始到結尾,兩個鏈表的長度一樣,那么兩個鏈表長度之差,也就是兩個鏈表從開頭到交集長度的差。所以我采取的方法是,將長度都算出來,將較長的那個鏈表的指針往前移difference位,然后兩個指針一起出發,相交點即為所求節點:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
if (!headA || !headB) return nullptr;
ListNode *copy_A = headA, *copy_B = headB;
int len_A = 0, len_B = 0;
while (headA->next) { headA = headA->next; ++len_A; }
while (headB->next) { headB = headB->next; ++len_B; }
if (headA != headB) return nullptr;
int dif = abs(len_A - len_B);
if (len_A > len_B)
{
while (dif--) copy_A = copy_A->next;
}
else
{
while (dif--) copy_B = copy_B->next;
}
while (copy_A->next)
{
if (copy_A == copy_B) return copy_A;
copy_A = copy_A->next;
copy_B = copy_B->next;
}
return copy_A;
}
};
這個代碼很長很難看,后來去discussion區看了一下,有更精簡的表述方法:原解答網址。Code摘抄如下:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
ListNode *p1 = headA;
ListNode *p2 = headB;
if (p1 == NULL || p2 == NULL) return NULL;
while (p1 != NULL && p2 != NULL && p1 != p2) {
p1 = p1->next;
p2 = p2->next;
// 如果從鏈表開頭到交集的長度一致,那么這一行就返回所找到交集節點
// 當兩者沒有交集,也用了這一行
if (p1 == p2) return p1;
// 如果短鏈表提前走完了,將指針置于長鏈表開端
// 等到長鏈表的指針也走完了,將指針置于短鏈表開端
// 而這時候,之前放置的長鏈表指針已經多走了difference步了
if (p1 == NULL) p1 = headB;
if (p2 == NULL) p2 = headA;
}
return p1;
}
138. Copy List with Random Pointer
A linked list is given such that each node contains an additional random pointer which could point to any node in the list or null.
Return a deep copy of the list.
這道題的code很簡單不貼了,思維過程值得回顧一下。
分析題意:
- deep copy:
- 說明需要新建node,而不只是對pointer的操縱
- 說明原來的node不能變動
- next pointer沒什么花頭,關鍵點在于怎么去對應這個random pointer。由于原來的random pointer指的是原來的node list中的node,現在的random pointer指的是新node list中的node,需要一種方法將原來的node和新的node一一對應
我一開始做的時候,想的是,將原來node的next pointer指到對應的現在node,現在node的random pointer指到原來node。這樣一來,上下互通。但是這個問題是,一旦現在node的random pointer被修改了以后,就無法復原原來的pointer。所以這種方法不對。
看了答案,是將新node插入進兩個老node里。另外值得注意的是,寫code的時候對于nullptr這種情況要分外注意,有兩次提交失誤都是因為沒有注意nullptr。
234. Palindrome Linked List
Given a singly linked list, determine if it is a palindrome.
Follow up:
Could you do it in O(n) time and O(1) space?
這道題是檢驗前面幾題有沒有白做了的范例。前面幾題用到的知識中最重要的亮點——fast/slow pointer; use a prev pointer in singly linked list。將這兩點與這題相結合,得出方法:用slow/fast pointer法+reverse list法將前半個list翻轉,然后從中間向兩邊對照數字。其中注意奇偶不同和nullptr的檢驗。
19. Remove Nth Node From End of List
Given a linked list, remove the nth node from the end of list and return its head.
For example,
Given linked list: 1->2->3->4->5, and n = 2.
After removing the second node from the end, the linked list becomes 1->2->3->5.
Note:
Given n will always be valid.
Try to do this in one pass.
巨喜歡這道題,因為它很靈活地考察了fast slow pointer??吹竭@道題的時候,我知道找到這個node之后,操縱pointer是很輕松的事情。主要是怎么來找到這個node。如果N是開頭到這個node的距離,那么一遍就能找到并且刪掉?,F在N是node到結尾的距離,我第一反應肯定要用到fast slow pointer,但是兩倍的fast slow pointer很難應用啊。然后想了幾種很復雜的方法,覺得不太符合題目“in one pass”的要求。然后也有點趕時間,就看答案了。
我來試圖模擬得到這個答案的思維過程:如果node到結尾的距離是N,整個list長L,那么從開頭到這個node的距離是L-N。也就是讓pointer走L-N距離就可以找到這個node了。讓pointer走L-N距離有兩種方法,一種是從開頭走到這個node,一種是從開頭+N走到最后。所以我們只要先將fast pointer往前挪N,然后跟slow pointer一起每次一格走到最后,slow pointer所得到的就是要被刪的node。
這道題屬于靈活應用的“微創新”題,真的很喜歡,希望下次自己能想出來。砰砰砰。
23. Merge k Sorted Lists
Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.
這道題其實挺有意思的。從merge two lists到merge k lists,第一個想到的是divide and conquer,把問題分解成merge two lists就好了。這樣的解法空間復雜度是O(logk),k是lists中list的數量,時間復雜度是O(nlogk),n是總共的node數,因為每一層merge的時候復雜度都是O(n),一共有logk層。另外還有一種解法是用priority queue,這個利用了priority queue的自帶sorting性質,每次我們將list的開頭push到這個queue里,queue里最多有k個node。code挺簡單的就不貼了。關于priority queue的底層implementation抽空再復習一下。
<Reorder list>
有不少題是關于調換linked list中元素的位置的,甚至是將list轉換為tree。這種題啊換湯不換藥??炻羔槨⒃O置prev pointer,可以說是套路至極了。
總結:
1、多想想fast/slow pointer,其中包括fast pointer比slow pointer跑的快一倍的(用于找中間數,找到cycle等),也包括fast比slow跑的不是兩倍的。這兩個指針可以用來解決很多在單向鏈表中關于距離的問題。
2、對于singly linked lists, 多想想設置一個prev pointer使之成為本質上的doubly linked lists
3、小心犯錯誤的code detail:
1)在操縱pointer的時候,小心前面的pointer變化影響到后面
2)考慮nullptr的情況
3)對一個node的前后node要多檢查