乾貨||鍊表的技巧和算法總結

2021-02-21 嵌入式ARM

鍊表的操作總結  鍊表反轉

這是一個簡單的鍊表操作問題,在leetcode上面有52.7%的通過率,難度是簡單。但是還是想在這裡基於python做一下總結,順便總結一下鍊表的各種操作。

首先先看一下leetcode上面的題目:

反轉一個單鍊表。

示例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
進階:
你可以迭代或遞歸地反轉鍊表。你能否用兩種方法解決這道題?

看完了題目,很直白,就是轉過來。我們嘗試對這道題進行解決。這道題用python至少會有三種解決方案。

首先是鍊表的數據結構定義:

1.將鍊表遍歷進list中,然後利用切片反轉list,再將list填充到鍊表中即可。這是最簡單的一種思考邏輯,但是也比較消耗性能。時間和空間複雜度都為O(n)。

2.另一種迭代算法,是一種純粹交換的迭代,筆者這裡截取了leetcode速度最快的一種。

這一波交換操作,我們可以畫個示意圖就知道他的交換是一種怎麼樣的交換。

從圖中可以看出,循環的作用就是將反向指針進行保存。同時令將指針轉向的功能。

3.最後一種方案是採用遞歸的方式進行鍊表反轉。這種方式也需要一定的理解。我們先展示一下代碼。

這種解法其實理解起來只有兩部分內容,傳遞反向指針和進行指針反向拼接。我們先來理解一下指針反向拼接這個操作。

如此循環即可將鍊表反轉過來。但是還有個關鍵就是將最後一個指針傳遞出來。我們可以看到之前的代碼中,end傳出來後是一直沒有做任何操作的。不停的return出最後一個指針。所以就將最後一個指針傳遞了出來。

以上就是鍊表反轉的3中方法。除此之外還想寫一些鍊表的簡單操作。

  快慢指針

何為快慢指針,即對鍊表進行兩個不同步頻的指針標記遍歷。最經典的是慢指針走一步,快指針走兩步。

快慢指針有很多的應用,比如說:

1.判斷一個鍊表是否存在環。

兩個指針並排走,如果有環的話快指針最終會指向慢指針的位置。沒有環的話,快指針會指向None後退出。

當然這道題的解法不止這一樣,還可以利用set進行判斷。

2.輸出鍊表中的倒數第K個節點

這道題利用快慢指針的思路是這樣的。定義兩個指針,第一個指針向前走k-1步;第2個指針保持不動;到第k步時第2個指針也開始移動。由於兩個指針始終保持著k-1的距離,所以當快指針到達末尾時,慢指針剛好指向倒數第k個。

鍊表的技巧總結


  技巧一 :理解指針或者引用的含義


看懂鍊表結構不是很難,但是一旦把它和指針混在一起,就很容易讓人摸不著頭腦,(我再寫代碼的時候就出現了這種情況),所以要想寫對鍊表代碼,首先要理解好指針。

實際上,對於指針的理解,你只需要記住下面這句話就可以了:將某個變量賦值給指針,實際上就是將這個變量的地址賦值給指針,或者反過來說,指針中存儲了這個變量的內存地址,指向了這個變量,通過指針就能找到這個變量。

  技巧二:警惕指針丟失和內存洩露


在寫鍊表代碼的時候,經常會找不到指針(引用)指到了哪兒,會弄丟了指針。

例如在單向鍊表中插入節點x,前節點是p,後節點是b。

p.next = x ;

x.next = p.next;

這樣就會導致指針丟失和內存洩露。如果把兩行代碼的順序顛倒一下,就不會丟失指針,要先將x.next指向b,然後,在將p的next節點指向x,這樣才不會丟失指針導致內存洩露。

  技巧三:利用哨兵簡化實現的難度


哨兵,解決的是國家之間的邊界問題。同理,這裡說的哨兵也是解決"邊界問題"的,不直接參與業務邏輯。(還不是很了解,之後再補充)。
技巧四:重點留意邊界條件處理

軟體開發中,代碼在處理一些邊界問題或者異常情況中,容易出現bug。鍊表代碼也是容易產生bug,要想實現沒有bug的鍊表代碼,一定要在編寫的過程中以及在編寫完之後,檢查邊界條件是否考慮的全面,以及代碼在邊界條件下是否能正常運行。

經常用來檢查鍊表代碼是否正確的邊界條件有如下幾個:

如果鍊表為空,代碼是否能正常運行?

如果鍊表只包含一個節點時,代碼是否正常運行?

如果鍊表只包含兩個節點時,代碼是否正常運行?

代碼在處理頭節點和尾節點時,代碼是否正常運行?


我們在寫代碼的時候也不要只是實現業務邏輯就完事,也要多考慮會遇到哪些邊界問題或者異常情況,遇到了應該如何解決,這樣寫出來的代碼才會健壯。

  技巧四、留意邊界的處理

比如鍊表為空、比如只包含一個結點或兩個節點的情況。比如處理頭結點和尾節點時,代碼是否正確。

再就是多寫多練了,這裡給出java語言實現的鍊表代碼。

  技巧五:舉例畫圖,輔助思考


舉例畫圖,這個太有用了,我再學習算法的過程中,有時候看不懂實現代碼的時候,就想著如何通過畫圖來分解代碼的實現步驟,感謝在學習中,幫助過我的朋友,讓我能夠理解實現的過程。

在這裡繼續用上述的有序鍊表的合併的代碼:

畫圖輔助思考:

我們可以看到這個代碼裡就體現了邊界問題的幾個步驟:

1、首先是鍊表為空時的處理。

2、鍊表頭的處理。

3、鍊表多節點的處理。

4,鍊表尾的處理。

而圖中的舉例也正是對代碼實現的分解的很好的解釋。(畫圖輔助真的很棒) 

  技巧六:多謝多練,沒有捷徑


就是把常見的鍊表操作都自己多寫幾遍。最開始我都是遇到了各種各樣的不理解,甚至對於鍊表的操作都有些迷糊,但是多出問題多調試,慢慢的我們也能孰能生巧。唯手熟爾!

常見的鍊表操作,只要能熟練的寫出來,不熟就多寫幾遍,保證之後就不會再害怕寫鍊表代碼。

單鍊表反轉

檢測鍊表中的環

兩個有序鍊表的合併

刪除鍊表中倒數第K個節點

球鍊表的中間節點

寫出正確鍊表代碼的六個技巧。分別是理解指針或引用的含義、警惕指針的丟失和內存洩露,利用哨兵簡化實現難度,重點留意邊界條件處理,以及舉例畫圖,輔助思考,還有就是多寫多練,唯手熟爾。

勤能補拙,生活就是養成遊戲,勤練內功,即使當前不能花裡胡哨,未來也會強壯到無人能敵!

鍊表的經典技巧及算法

  1、尋找鍊表的中間節點:最簡單的方法是,先遍歷一遍鍊表,計算出鍊表的長度,然後計算出中間節點的位置,然後再遍歷一遍,邊遍歷邊計算,直到找到中間節點,這個方法略顯囉嗦,最壞的情況需要遍歷2次鍊表,代碼如下:    

另一個更靈巧的方法是,用兩個指針,慢指針每次走一步,快指針每次走兩步,當快指針走到鍊表的末端(NULL)時,慢指針正好指向了中間節點,代碼如下:

  2、檢測鍊表是否有環:經典的做法也是用快慢指針,如果沒有環,快指針一定先到達鍊表的末端(NULL),如果有環,快、慢指針一定會相遇在環中,代碼如下:

  3、檢測環的入口:經典的做法是先檢測是否有環,如果有環,則計算出環的長度,然後使用前後指針(不是快慢指針),所謂的前後指針就是一個指針先出發,走了若干步以後,第二個指針也出發,然後兩個指針一起走,當前後指針相遇時,它們正好指向了環的入口,代碼如下:

如果允許使用額外的內存,可以有更簡單的做法,即一邊遍歷,一邊將節點放在map中,當某個節點第二次出現在map中時,它就是入口節點,代碼如下:

  4、鍊表翻轉:假設原鍊表為1->2->3,翻轉以後的鍊表應該是1<-2<-3,即節點3變成了頭節點,代碼如下:

  5、刪除鍊表中的節點,注意這裡只給出要刪除的那個節點,不給出鍊表頭(假設被刪除節點不是尾節點),代碼如下:

如果被刪除節點是尾節點,上面的代碼就無法將target上一個節點的next置為NULL,所以只有給了頭節點後,才能遍歷到target的上一個節點,並把其next置為NULL。

  6、回文鍊表的檢測:所謂回文鍊表,即鍊表元素關於中間對稱,,如1->2->3->2->1,比較簡單的方法是用一個棧,先順序遍歷鍊表,並把每個節點放入棧中,遍歷完成後,棧的出棧順序正好是原鍊表的逆,代碼如下:

上面代碼的空間複雜度為O(N),其實還有空間複雜度為O(1)的算法,也很靈巧,運用了之前提到的一些技巧,代碼如下:

這個方法的缺點是修改了原鍊表,但是綜合運用了鍊表的很多技巧,值得收藏。

  7、合併有序鍊表:基本思路跟合併有序數組一樣,但是不需要O(N)的空間複雜度了,只需要O(1)的空間複雜度,代碼如下:

其實如果不要求空間複雜度為O(1),可以用遞歸的思想,代碼更簡略,如下:

  8、鍊表排序:如果沒有空間複雜度、時間複雜度的要求,那可選的方法太多了,像插入排序、選擇排序、冒泡排序,但是如果要求時間複雜度為O(NlogN),而且空間複雜度為O(1)呢?歸併排序!!!正好可以用上剛剛寫的合併有序鍊表的代碼,代碼如下:

  9、鍊表的循環右移:舉例如下1->2->3->4->5->NULL,循環右移2位後,變成了4->5->1->2->3->NULL,可以這麼考慮,如果鍊表的長度為N,循環右移K位,那麼等效於循環右移 K%N位,K%N是一個小於N的數,然後我們只需要找到循環右移後的頭節點即可,上面的例子就是4,然後直接把1->2->3連結到4->5->的後面,代碼如下:

  10、以組為單位翻轉鍊表:組的長度用K表示,比如原鍊表為1->2->3->4->5,當K=2時,翻轉的結果為2->1->4->3->5,當K=3時,翻轉的結果為3->2->1->4->5,即先翻轉K個,再翻轉K個,當剩下的節點數小於K時,就不用翻轉了。用遞歸的方法很容易實現,代碼如下:

這個算法的空間複雜度為O(N/K),即正比於遞歸深度,如果要求空間複雜度為O(1)呢?其實也比較簡單,只要循環處理每一段長度為K的鍊表,處理的時候注意保存上一段鍊表的尾節點,代碼如下:

11、翻轉鍊表的相鄰節點,比如原鍊表為1->2->3->4,翻轉後為2->1->4->3,這個其實就是上一道題的特例,即K=2,也要求空間複雜度為O(1),不過還是遞歸簡潔啊,這裡只給出遞歸的代碼:

  12、刪除鍊表的倒數第N個節點,要求只遍歷一次,還記得檢測環的入口嗎?是的,用前後指針,前後指針需要相隔(N+1)步,這樣當前指針為NULL的時候,後指針正好指向倒數第(N+1)個節點,然後直接刪除倒數第N個節點即可,代碼如下:

  13、刪除有序鍊表中的重複元素,如原鍊表為1->2->3->3->4->4->5,刪除後的鍊表為1->2->5,這道題的關鍵是如果某節點有重複,務必將其全部刪掉,所以要對有重複的節點做個標記,代碼如下:

技巧:啞變量的引入使得頭結點不再具有特殊性,從而簡化處理流程。

  14、像快排那樣將鍊表分成前後兩個部分,比如原鍊表為1->4->3->2->5->2,給出數字3,那麼鍊表中比3小的放在前面,比3大(或等於3)的放在鍊表的後面,處理後的鍊表應該是這樣的1->2->2->4->3->5,注意,4和5都大於3,那麼處理後的鍊表中4仍然應該在5的前面,代碼如下:

  15、合併K個有序鍊表,這個咋一聽很簡單,先合併第1個、第2個,然後將合併後的結果與第3個合併,然後將合併的結果與第4個合併……,假設每個鍊表的長度為N,那麼時間複雜度為O(NK²),N為總的節點數,因為要合併K次。其實有更優的時間複雜度,我們可以先兩兩合併,即第1個與第2個合併,第3個與第4個合併,即執行K/2次合併,這作為第一輪合併,時間複雜度為O(KN),接下來就只需要合併K/2個有序鍊表了,即進行第二輪合併,這樣總共需要進行logK輪合併,每一輪的時間複雜度為O(KN),所以總的時間複雜度為O(NKlogK),代碼如下(合併兩個有序鍊表的代碼已經在前面給出):

  16、尋找兩個鍊表的第一個公共節點,即兩個鍊表從某個節點開始合併了,我們要找出這個節點,經典的方法是先計算兩個鍊表的長度:L1,L2,假設L1>L2,那麼公共節點一定不在鍊表1的前(L1-L2)個節點中,這樣我們就可以讓鍊表1的頭節點指向第(L1-L2+1)個節點,然後同時推進兩個鍊表的頭節點,邊推進邊比較,直到遇到同一個節點,代碼如下:

  17、鍊表的插入排序,這個就很直白了,代碼如下:

  18、把有序鍊表轉換成一個儘量平衡的二叉樹,其實所謂的儘量平衡,就是把鍊表的中間節點作為根節點,根節點左邊的鍊表是樹的左子樹,根節點右邊的鍊表是樹的右子樹,然後問題就轉換成了原問題的子問題,即用前半段鍊表建立一個儘量平衡的左子樹,用後半段鍊表建立一個儘量平衡的右子樹,代碼如下:

  19、在鍊表上模擬加法運算,比如(2 -> 4 -> 3) + (5 -> 6 -> 5)=7 -> 0 -> 9,鍊表的頭節點為個位,然後是十位……其實模擬的關鍵就是處理進位,代碼如下(略長,但比較直白):

信息整理自網絡

實踐出真知▼

相關焦點

  • 有關鍊表的小技巧,我都給你總結好了
    提到鍊表就不得不提數組,它和數組可以說是數據結構的基礎,那麼它們最主要的區別在於:所以數組最好的性質就是可以隨機訪問 random access,有了 index,可以 O(1) 的時間訪問到元素。今天我們來說鍊表中最主要的 2 個技巧:雙指針法和 dummy node,相信看完本文後,鍊表相關的絕大多數題目你都能搞定啦。
  • 【算法系列】面試常用數據結構(二):鍊表
    Hello 大家好我是今天沒寫 bug 的八哥,上周我們總結分享了面試高頻數據結構中的數組和避坑指南,如果你還沒有閱讀的話快點擊下面圖片進行閱讀吧。之前我們講過,數據結構只有兩種:一維數據結構和多維數據結構。一維數據結構最重要的是連續性的數組和非連續性的鍊表,這種連續性和非連續性的特點會一直影響及應用在對應的多維數據結構中。
  • 圖解堆算法、鍊表、棧與隊列
    辨析棧與隊列棧和隊列學過沒學過算法的應該都聽過棧和隊列了吧,往往容易弄混的就是「後進先出」和「先進先出」了。今天又看到了「河內塔」的相關資料,也被稱為「漢諾塔」等。於是就想到了畫出下面這樣的圖案。大家看著圖片應該也都知道這分別是哪種鍊表了。那麼鍊表到底是什麼呢?它和前面的棧和隊列一般,都是基本的數據結構,其中的各個對象按線性順序排列。
  • 面試官系列 - LeetCode鍊表知識點&題型總結
    有一些讀者反饋說,能不能整理一些面試常見的算法。前段時間,我恰好總結了 LeetCode 常見的面試算法題目。今天分享給大家。剛開始準備刷算法題目的時候,感覺真的是好難,十道題目有九道是不會的。心中曾一萬隻草泥馬跑過,自己怎麼這麼辣雞。慢慢得,我發現算法也是一個可以通過練習慢慢成長的。
  • 算法一看就懂之「 數組與鍊表 」
    其實也不是真的用得少,只不過我們在使用的時候被很多高級語言和框架組件封裝好了,真正需要自己去實現的地方比較少而已。但別人封裝好了不代表我們就可以不關注了,數據結構作為程式設計師的內功心法,是非常值得我們多花時間去研究的,我這就翻開書複習複習:本文就先從大家最經常使用的「 數組 」和「 鍊表 」聊起。不過在聊數組和鍊表之前,咱們先看一下數據的邏輯結構分類。
  • LeetCode 例題精講 | 05 雙指針*鍊表問題:快慢指針
    點擊關註上方「五分鐘學算法」,設為「置頂或星標」,第一時間送達乾貨。只要在解題時用到了兩個指針(鍊表指針、數組下標皆可),都可以叫做雙指針方法。根據兩個指針運動方式的不同,雙指針方法可以分成同向指針、對向指針、快慢指針等。當雙指針方法遇到鍊表問題,我們主要使用快慢指針方法。很多時候鍊表操作遇到疑難雜症時,可以使用快慢指針,減少算法所需的時間或者空間。
  • 給Java程式設計師的20個鍊表面試題
    什麼是鍊表?數據結構在程序面試中極其重要。鍊表則是對數組數據結構進行的補充,是另一種常見的數據結構。和數組相似的是,鍊表也是線性數據結構,並以線性方式儲存元素。但是,和數組不同的是,鍊表不將元素儲存在連續的位置;相反,其元素分散在內存中的各個地方,並以節點進行相互連接。鍊表不過是一個節點的列表,其中每一個節點都包含存儲的值和下一個節點的位置。
  • 什麼是鍊表?
    ,修煉編程內功)來源:碼海如果說數據結構是算法的基礎,那麼數組和鍊表就是數據結構的基礎。因為像堆,棧,樹,圖等比較複雜的數組結基本上都可以由數組和鍊表來表示,所以掌握數組和鍊表的基本操作十分重要。接下來我們來看看鍊表的表現形式和解題技巧。鍊表的表示由於鍊表的特點(查詢或刪除元素都要從頭結點開始),所以我們只要在鍊表中定義頭結點即可,另外如果要頻繁用到鍊表的長度,還可以額外定義一個變量來表示。
  • 算法入門必讀:Merge Two Sorted Lists(合併兩個有序鍊表)
    我們最先想到的應該是會用暴力方法來解決這個問題,即將直接將L2鍊表連接到L1鍊表後,形成新的鍊表,然後對新的鍊表進行排序。雖然這個方法是可行的,但是這個方法並不是最好的,這個方法主要的耗時在於重新排序,它的時間複雜度是O(m+n)log(m+n),m和n分別是L1和L2鍊表的長度,稍後我們會給出更加優雅的方案以供討論。
  • 鍊表算法面試問題?看我就夠了!
    1 引言 單鍊表的操作算法是筆試面試中較為常見的題目。本文將著重介紹平時面試中常見的關於鍊表的應用題目。本文大概 一萬五千字 ,建議閱讀時間為一個小時,請先收藏再閱讀,平時也可以拿出來多看幾遍。2 輸出單鍊表倒數第 K 個節點 2.1 問題描述題目:輸入一個單鍊表,輸出此鍊表中的倒數第 K 個節點。(去除頭結點,節點計數從 1 開始)。
  • 數據結構和算法學習指南
    首先,這裡講的都是普通的數據結構和算法,咱不是搞競賽的,野路子出生,只解決常規的問題,以面試為最終目標。另外,以下是我個人的經驗的總結,沒有哪本算法書會寫這些東西,所以請讀者試著理解我的角度,別糾結於細節問題,因為這篇文章就是對數據結構和算法建立一個框架性的認識。
  • 數組和鍊表pk篩選法
    數組和鍊表
  • 動畫 | 什麼是鍊表?
    如果說數據結構是算法的基礎,那麼數組和鍊表就是數據結構的基礎。因為像堆,棧,樹,圖等比較複雜的數組結基本上都可以由數組和鍊表來表示,所以掌握數組和鍊表的基本操作十分重要。接下來我們來看看鍊表的表現形式和解題技巧。鍊表的表示 由於鍊表的特點(查詢或刪除元素都要從頭結點開始),所以我們只要在鍊表中定義頭結點即可,另外如果要頻繁用到鍊表的長度,還可以額外定義一個變量來表示。
  • 數據結構與算法大總結
    數據結構知識綜述一、前言經過一個學期對數據結構與算法的學習,對這門課程有了更深入的了解,了解到了數據結構的相關知識,明白了,數據結構的重要性及應用,回顧一個學期,通過這個小論文,來總結一學期的學習成果,數據結構這門課程,具有一定的深度和廣度,有其複雜性,我們一起來分析一下
  • 算法基礎知識 目錄
    主要包含以下內容:1,實現基本數據結構(面試常考,而做題沒有)2,Java 源碼分析,Java 基礎補充(面試常考,而做題沒有)3,在做題中的應用(Leetcode 題目)4,Java 基礎(零基礎起步,一天搞定)5,做題技巧,做題模版雖說是「基礎知識」,但講的內容都是面試曾經考過的內容。
  • TOP 48 算法和編程面試題,牛逼啊!
    編程面試題通常包含數據結構和基於算法的問題,以及一些邏輯問題,例如:如何在不使用臨時變量的情況下交換兩個整數?為了清晰,編程面試題需要劃分為不同主題。我們在面試中經常看到的領域是數組、鍊表、字符串、二叉樹以及有關算法的問題(例如字符串算法、快速排序或基數排序等排序算法),本文將介紹這些內容。
  • 微信大佬總結的算法學習經驗
    ,他在微信待了八年,這次聊了挺多,收穫挺多,今天分享一篇他的算法學習經驗出來,希望對大家有幫助~這篇就來說說算法刷題方面的一些經驗和技巧。以下的經驗技巧,對於算法新手,或大學沒有搞過ACM,想利用業餘時間提升算法能力的同學比較有幫助,對於算法高手和ACM大牛,可能不太適用,僅供參考。
  • 精選算法面試-鍊表(反轉)
    一.簡介挑選了幾道常考的反轉鍊表題,鍊表是通過指針將一組零散的內存串聯在一起,反轉充分了利用指針來操作。給定一個鍊表,兩兩交換其中相鄰的節點,並返回交換後的鍊表,你不能只是單純的改變節點內部的值,而是需要實際的進行節點交換。
  • 算法題就像搭樂高:手把手帶你拆解 LRU 算法
    LRU 算法就是一種緩存淘汰策略,原理不難,但是面試中寫出沒有 bug 的算法比較有技巧,需要對數據結構進行層層抽象和拆解,本文 labuladong 就給你寫一手漂亮的代碼。計算機的緩存容量有限,如果緩存滿了就要刪除一些內容,給新內容騰位置。但問題是,刪除哪些內容呢?我們肯定希望刪掉哪些沒什麼用的緩存,而把有用的數據繼續留在緩存裡,方便之後繼續使用。
  • 奇偶鍊表 | Python
    奇偶鍊表題目來源:力扣(LeetCode)https://leetcode-cn.com/problems/odd-even-linked-list/ 題目 給定一個單鍊表,把所有的奇數節點和偶數節點分別排在一起。