Linked Lists

花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要多檢查

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容