1. 程式人生 > >資料結構——動手實戰雙向連結串列

資料結構——動手實戰雙向連結串列

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注
在之前介紹SkipList的文章當中,有一些同學反饋說由於對連結串列缺少認知以及瞭解,所以直接啃演算法有些過於困難。加上之前的文章當中介紹過了棧,所以這次繼續線性表這個話題,我們來一起討論一下**連結串列**。 連結串列是很多資料結構的基礎,它的最大特點是支援快速的刪除和插入,因此在很多資料頻繁變動的場景下使用廣泛。而且連結串列的**可拓展性較強**,所以它的應用非常廣泛,相關的拓展和改進版本也很多。今天我們和大家介紹的是雙端連結串列,也稱為雙向連結串列,它是尋常單向連結串列的改進版本,也是會經常使用的連結串列。
## 單向連結串列
連結串列(Linked list)是一種常見的基礎資料結構,是一種線性表,但是並不會按線性的順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer)。以上是維基百科當中的定義,我們可以明白兩點,首先連結串列由多個節點組成的,每個節點儲存了下一個節點的位置。其次,連結串列的各個節點不是順序儲存的。 我們看一下下圖可以加深一下了解: ![](https://user-gold-cdn.xitu.io/2020/3/5/170a820850ef2af7?w=670&h=268&f=png&s=47000) 上圖當中展示是單向連結串列,單向的意思是說每個節點只有一個指向後繼節點的指標,也就是說連結串列只有一個遍歷方向,因此稱為是單向連結串列。
## 連結串列的增刪
初學者在學習連結串列時可能會頭疼它的使用,相比於陣列的直接訪問,連結串列需要通過移動指標來遍歷節點修改節點的內容來完成增刪,因此不如陣列直觀。我一直想要找到一個很好的例子來比喻連結串列執行的機制,直到有一次看諜戰片,我發現間諜聯絡的系統就是以連結串列形式工作的。所以我決定以間諜系統來舉例,介紹一下連結串列的工作原理。 假設你是民國時期**軍統的頭目**,你負責一個諜報鏈路。為了安全,你和你的手下們是**單向聯絡**的。也就是說只能你聯絡你的一個手下,由這個手下再去聯絡其他人,最終把暗號交到目標的手上。假設你的代號是A,你的手下是B,B的手下是C,C來執行任務。這個機制一直運轉很好,直到有一天,B因為神祕原因轉移了地點,導致A和B聯絡的時間變長,為了解決這個問題,你決定新增一個人手D專門負責A和B之間的聯絡,加快聯絡速度。你應該怎麼辦呢? 由於身份限制,以及安全原因,你是不知道B的具體資訊的。你只能將訊息給到A,讓A去聯絡新人D,並且告訴他B的聯絡方法。還有一點需要注意,A必須等到D成功找到了B之後再切斷和B的聯絡。否則一旦D出現什麼意外,這整條鏈路就斷了。 我們把整個圖畫出來如下: ![](https://user-gold-cdn.xitu.io/2020/3/5/170a82083f8f2263?w=452&h=411&f=png&s=13533) 先讓D和B取得聯絡,之後斷開A和B的聯絡。但是有一個問題,由於A的後繼只有一個,A如果指向D,那麼B的位置就會丟失。所以我們需要先用一個臨時變數cur儲存下來B的位置,然後再讓D指向這個cur。不過在Python當中,我們可以不用這麼麻煩,利用Python的多變數賦值的方法,我們可以一行程式碼搞定。 程式碼如下: ```python def add(pos, new_node): new_node.next, pos.next = pos.next, new_node ``` 假如過了一段時間,B又回到了原處,我們不需要D了,要刪除這個節點該怎麼辦?很簡單,直接讓D將B的最新的聯絡方式給A就可以了。也就是說讓A跨過D指向B即可。 來看圖: ![](https://user-gold-cdn.xitu.io/2020/3/5/170a8208485f4e9c?w=452&h=271&f=png&s=10788) ```python def delete(pos): pos.next = pos.next.next ```
## 雙向連結串列
理解了單向連結串列,雙向連結串列也就很簡單了。雙向的意思也很明顯,每個節點除了記錄後繼節點的位置之外,還會**記錄源頭**節點的位置。有了雙向指標之後不僅是獲取來源節點方便而已,並且也可以很方便地對整個連結串列進行倒敘遍歷和頭部插入。 還記得我們之前Python專題當中介紹過的**deque**這個庫嗎?通過deque我們可以實現一個雙向增刪元素的佇列,結合雙向連結串列的定義,很容易發現deque其實就是保留了一部分api的雙向連結串列。換句話說deque是基於雙向連結串列實現的,就和棧是基於list實現的一樣。 和單向連結串列相比,由於我們多了一個指標,理解和實現起來會更加容易,因為之前需要通過順序關係以及臨時變數完成的內容現在可以通過前向指標很輕易地實現了。 下面附上雙向連結串列增刪的程式碼: ```python def add(pos, new_node): pos.next, new_node.next = new_node, pos.next new_node.pre, new_node.next.pre = pos, new_node def delete(pos): pos.next = pos.next.next # 這個時候pos.next已經更新 pos.next.pre = pos ```
## 總結
雙向連結串列本身並不複雜,也沒有太多變化的花樣,和之前介紹的SkipList相比要簡單許多。我相信即使是初學者,只要自己動手實現一遍,也足夠掌握。在我初學資料結構的時候,我非常抗拒使用連結串列,除了覺得定址很麻煩,需要遍歷整個連結串列耗時很大之外。另一個根本的原因是在C++當中連結串列的編寫很麻煩,而且很容易有**記憶體洩漏以及野指標**問題。所以我當時儘可能地使用陣列作為替代,並且甚至一度認為隨著記憶體價格的降低,總有一天我們可以拋棄連結串列這個結構。 直到後來我學習了作業系統之後,我找到了一個必須使用連結串列的理由。因為在作業系統當中,**記憶體並不是連續的**,大部分記憶體都是分散的。當我們建立一個數組的時候,我們其實是在想作業系統申請一塊連續的記憶體。我們申請的陣列越大,這塊記憶體也就越大。顯然越大的記憶體越難申請到,因為記憶體大多被切分成了許多碎片,在資源不夠的情況下,作業系統需要做大量的工作才能將碎片蒐集起來。 而連結串列因為通過指標定址,所以可以避免這個問題,連結串列當中的元素分散在記憶體各處,分攤了記憶體消耗的壓力。這也是在作業系統領域當中,連結串列大量使用的原因。 最後,由於篇幅限制,我們沒有放上全部的程式碼,想要獲取完整雙向連結串列程式碼的同學,可以關注我的公眾號,回覆“**雙向連結串列**”獲取。 今天的文章就到這裡,如果覺得有所收穫,請順手點個**關注或者轉發**吧,你們的舉手之勞對我來說很重要。 ![](https://user-gold-cdn.xitu.io/2020/3/5/170a82148acda9d9?w=258&h=258&f=png&