前言
又是很長時間才回來發一篇博客,前一個月確實因為雜七雜八的事情影響了很多,現在還是到了大火燃眉毛的時候了,也應該開始繼續整理一下算法的思路了。Linked List大家應該是特別熟悉不過的了,因為這個算是數據結構了里面基本上最開始講的結構吧。這塊內容也沒有太多需要琢磨的技巧,可以考量的東西也不多,所以考的就是一些小的trick來完成,面試中鏈表考得特別多,算是面試官對面試者的基礎的考查,所以我建議大家在Linked List這一章,一定要實現Bug Free。這個也是我練的比較多的,有些想法可以和大家分享。
outline:
- Dummy Node in Linked List
- Remove Duplicates from Sorted List II
- Reverse Linked List II
- Partition List
- Basic Linked List Skills
- Sort List
- Reorder List
- Two Pointers in Linked List (Fast-slow pointers)
- Merge K Sorted Lists
課堂筆記
1. Dummy Node in Linked List
有很多時候,我們需要對整個鏈表進行操作,這樣會導致鏈表的結構發生變化,或者當需要返回的鏈表頭是不確定的時候,我們就需要用一個Dummy Node來存那個開始的“頭”,最后返回的也是這個“頭”。這樣就不需要單獨對head進行操作了,第一個題目如下:
Remove Duplicates from Sorted List II
給定一個排序鏈表,刪除所有重復的元素只留下原鏈表中沒有重復的元素。
樣例
給出 1->2->3->3->4->4->5->null,返回 1->2->5->null
給出 1->1->1->2->3->null,返回 2->3->null
這個題算是比較簡單的題目,就是考慮鏈表的刪除操作。但是如果不用dummy的話,可能還需要單獨考慮head是否為重復元素,部分代碼如下:
int val = head->val; while (head->next && head->next->val == val) { head->next = head->next->next; } head = head->next;
這樣就很麻煩,代碼不夠簡潔,而且可能在某些地方出現問題。所以這里就引入dummy的方法(這塊內容一定要朗讀並背誦全文)(Bug Free):

ListNode * deleteDuplicates(ListNode *head) { // write your code here if (!head || !head->next) { return head; } ListNode dummy = ListNode(0); dummy.next = head; head = &dummy; while (head->next && head->next->next) { if (head->next->val == head->next->next->val) { int val = head->next->val; while (head->next && head->next->val == val) { head->next = head->next->next; } } else { head = head->next; } } return dummy.next; }
這里的思想比較簡單,就是用一個dummy存入一開始head的地址,然后再把head指向dummy,這樣就相當於整個鏈表從head->next開始了,然后進行相應的判斷就好。最后返回dummy.next即為鏈表當前的head。其實就是一個小技巧,大家就可以把head當成一個p節點,直接使用就好。
接下來一個題:
Reverse Linked List II
翻轉鏈表中第m個節點到第n個節點的部分
樣例
給出鏈表1->2->3->4->5->null, m = 2 和n = 4,返回1->4->3->2->5->null
相信大家在面試的時候被問到鏈表反轉的時候一定是最高興的,因為這時候你可以5分鍾把bug free的代碼寫出來。這道題稍微增加了一點點難度,就是反轉從m到n的節點,其他的不變。其實也不難,就是保留一下關鍵的點,然后其他地方一樣進行反轉就可以。這個題也是為了講解一下dummy,所以放出來,代碼如下(Bug Free):

ListNode *reverseBetween(ListNode *head, int m, int n) { // write your code here if (!head || !head->next) { return head; } ListNode dummy = ListNode(0); dummy.next = head; head = &dummy; for (int i = 1; i < m; ++i) { head = head->next; } // 保存m前一個節點 ListNode *pre = head; // 保存第m個節點 ListNode *mNode = head->next; // 保存第n個節點 ListNode *nNode = mNode; // 保存n的后一個節點 ListNode *post = mNode->next; for (int i = m; i < n; ++i) { ListNode *tmp = post->next; post->next = nNode; nNode = post; post = tmp; } mNode->next = post; pre->next = nNode; return dummy.next; }
這里就不多解釋了。
這個小節主要就是讓大家熟悉掌握dummy的用法,這樣能夠大量介紹代碼量,並且給面試加分不少。
還是記住兩個點:
- 當鏈表的結構發生變化時
- 當需要返回的鏈表頭不確定時
這兩種情況下是需要使用dummy的。
這里最后來一個題:
Partition List
給定一個單鏈表和數值x,划分鏈表使得所有小於x的節點排在大於等於x的節點之前。
你應該保留兩部分內鏈表節點原有的相對順序。
樣例
給定鏈表 1->4->3->2->5->2->null,並且 x=3
返回 1->2->2->4->3->5->null
其實這道題是最能體現dummy的作用的。我們就遍歷整個鏈表,比x小的放入左邊的鏈表,否則放入右邊的鏈表,最后再把兩個鏈表合在一起。這時候使用dummy就不需要再去找各自的頭指針了。看似簡單,但是最后我還是沒有實現bug free,關鍵的地方就是:當遍歷到最后一個節點的時候,如果這個數比x小,那么需要把它向前移動,此時就需要把右邊鏈表的尾節點指向空,否則就會出現LTE,這個是非常關鍵的問題,需要大家重視。代碼如下:

ListNode *partition(ListNode *head, int x) { // write your code here if (!head || !head->next) { return head; } ListNode dummy1 = ListNode(0); ListNode dummy2 = ListNode(0); ListNode *pre = &dummy1; ListNode *post = &dummy2; while (head) { if (head->val < x) { pre->next = head; pre = head; } else { post->next = head; post = head; } head = head->next; } // 最后的節點指向空,否則出現死循環 post->next = NULL; pre->next = dummy2.next; return dummy1.next; }
2. Basic Skills in Linked List
說到鏈表的基本技巧,插入、刪除、翻轉這三個大家應該都很熟悉了,然后還有兩個比較麻煩的操作就是合並兩個鏈表或是中分兩個鏈表(這里用這樣的翻譯求不吐槽,原文:middle of a linked list),可以說這兩個技巧要是充分掌握了的話,基本上鏈表的題目你也不需要擔心了。
直接來一個題吧:
Sort List
在 O(n log n) 時間復雜度和常數級的空間復雜度下給鏈表排序。
樣例
給出 1->3->2->null,給它排序變成 1->2->3->null.
這個思路就不需要我講了,接下來我將用歸並排序來做,這里不適用快速排序是因為大量的操作對於鏈表來說耗時太大了,所以就不考慮了。
歸並排序(Bug Free):

ListNode *merge(ListNode *left, ListNode *right) { ListNode dummy = ListNode(0); ListNode *res = &dummy; while (left && right) { if (left->val < right->val) { res->next = left; left = left->next; } else { res->next = right; right = right->next; } res = res->next; } if (left) { res->next = left; } else { res->next = right; } return dummy.next; } ListNode *sortList(ListNode *head) { // write your code here if (!head || !head->next) { return head; } ListNode *fast = head->next; ListNode *slow = head; while(fast && fast->next) { fast = fast->next->next; slow = slow->next; } ListNode * right = sortList(slow->next); // 這里非常關鍵,一定要把左邊結尾指向空 slow->next = NULL; ListNode * left = sortList(head); return merge(left,right); }
接下來的一個題在面試中見到過,算是比較簡單,但是容易出錯的題目:
Reorder List
給定一個單鏈表L: L0→L1→…→Ln-1→Ln,
重新排列后為:L0→Ln→L1→Ln-1→L2→Ln-2→…
必須在不改變節點值的情況下進行原地操作。
樣例
給出鏈表 1->2->3->4->null,重新排列后為1->4->2->3->null。
這樣的題其實看上去比較復雜,但是你可以結合一下我前面講過的幾個題,就是一種非常簡單的做法就可以完成了。我們首先找到鏈表的中點,然后把右半段鏈表翻轉,之后再進行合並就可以,代碼如下(Bug Free):

void merge(ListNode *left, ListNode *right) { while (left && right) { ListNode *node1 = left->next; ListNode *node2 = right->next; left->next = right; right->next = node1; left = node1; right = node2; } } void reorderList(ListNode *head) { // write your code here if (!head || !head->next) { return; } ListNode *fast = head->next; ListNode *slow = head; while (fast && fast->next) { fast = fast->next->next; slow = slow->next; } ListNode *right = slow->next; slow->next = NULL; ListNode *ntr = NULL; while (right && right->next) { ListNode *tmp = right->next; right->next = ntr; ntr = right; right = tmp; } right->next = ntr; merge(head,right); }
這個題關鍵還是要細心,應該不會出什么問題的。
3.fast-slow pointers
寫了半天,突然chrome就崩潰了,真是好心塞。
這個因為之前也講過,所以直接來做一個比較重要的題目吧:
Merge K Sorted Lists
合並k個排序鏈表,並且返回合並后的排序鏈表。嘗試分析和描述其復雜度。
這個題目可以用優先隊列來做,也可以兩兩合並然后合在一起,這里我才用的是分治法來講解:把lists分解為k/2的規模,然后繼續分解,直至兩兩合並,思路比較簡單,代碼如下

ListNode *mergeTwoLists(ListNode *left, ListNode *right) { ListNode dummy = ListNode(0); ListNode *res = &dummy; while(left && right) { if (left->val < right->val) { res->next = left; left = left->next; } else { res->next = right; right = right->next; } res = res->next; } if (left) { res->next = left; } else { res->next = right; } return dummy.next; } ListNode *mergeHelper(vector<ListNode *> &lists, int start, int end) { if (start == end) { return lists[start]; } int mid = start + (end - start) / 2; ListNode *left = mergeHelper(lists, start, mid); ListNode *right = mergeHelper(lists, mid + 1, end); return mergeTwoLists(left,right); } ListNode *mergeKLists(vector<ListNode *> &lists) { // write your code here if (!lists.size()) { return NULL; } return mergeHelper(lists, 0, lists.size() - 1); }
總結
因為鏈表算是比較基礎的內容了,所以也不需要太多的東西。大家就把幾個重要的trick掌握好就可以了,還是需要多多練習。
知識點如下:
1.dummy的使用
2.fast-slow pointer的使用