九章算法系列(#5 Linked List)-課堂筆記


前言

又是很長時間才回來發一篇博客,前一個月確實因為雜七雜八的事情影響了很多,現在還是到了大火燃眉毛的時候了,也應該開始繼續整理一下算法的思路了。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;
    }
View Code

這里的思想比較簡單,就是用一個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;
    }
View Code

這里就不多解釋了。

這個小節主要就是讓大家熟悉掌握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;
    }
View Code

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);
    }
View Code

接下來的一個題在面試中見到過,算是比較簡單,但是容易出錯的題目:

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);
    }
View Code

這個題關鍵還是要細心,應該不會出什么問題的。


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);
    }
View Code

總結

因為鏈表算是比較基礎的內容了,所以也不需要太多的東西。大家就把幾個重要的trick掌握好就可以了,還是需要多多練習。

知識點如下:

1.dummy的使用

2.fast-slow pointer的使用

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM