《演算法與資料結構》學習筆記4 鍊錶(上)

2021-09-10 15:16:22 字數 4547 閱讀 3466

本次分兩篇來記錄鍊錶的內容。

與陣列相比,鍊錶是一種稍微複雜一點的資料結構。這兩個非常基礎、非常常用的資料結構,常常將會放到一塊兒來比較。兩者有什麼區別:

從底層的儲存結構上來看:從下圖中我們看到,陣列需要一塊連續的記憶體空間來儲存,對記憶體的要求比較高。如果申請乙個 100mb 大小的陣列,當記憶體中沒有連續的、足夠大的儲存空間時,即便記憶體的剩餘總可用空間大於 100mb,仍然會申請失敗。而鍊錶恰恰相反,它並不需要一塊連續的記憶體空間,它通過「指標」將一組零散的記憶體塊串聯起來使用,所以如果我們申請的是 100mb 大小的鍊錶,根本不會有問題

主要介紹三種鍊錶結構:單鏈表、雙向鍊錶、迴圈鍊錶。

單鏈表

鍊錶通過指標將一組零散的記憶體塊串聯在一起。其中,把記憶體塊稱為鍊錶的「結點」。為了將所有的結點串起來,每個鍊錶的結點除了儲存資料之外,還需要記錄鏈上的下乙個結點的位址。如圖所示,把這個記錄下個結點位址的指標叫作後繼指標 next。

其中有兩個結點是比較特殊的,它們分別是第乙個結點和最後乙個結點。習慣性地把第乙個結點叫作頭結點,把最後乙個結點叫作尾結點。其中,頭結點用來記錄鍊錶的基位址。有了它,就可以遍歷得到整條鍊錶。而尾結點特殊的地方是:指標不是指向下乙個結點,而是指向乙個空位址 null,表示這是鍊錶上最後乙個結點。

與陣列一樣,鍊錶也支援資料的查詢、插入、刪除。

在對陣列進行資料的插入刪除時,為了保持內在資料的連續性,需要做大量的資料搬移操作,所以時間複雜度為o(n),而在鍊錶中插入或者刪除乙個資料,我們並不需要為了保持記憶體的連續性而搬移結點,因為鍊錶的儲存空間本身就不是連續的。所以鍊錶的插入和刪除是非常快速的。

在對鍊錶進行插入刪除操作時,只需要考慮相信結點的指標改變,對應的時間複雜度為o(1)。如下圖所示。

鍊錶要想隨機訪問第 k 個元素,就沒有陣列那麼高效了。因為鍊錶中的資料並非連續儲存的,所以無法像陣列那樣,根據首位址和下標,通過定址公式就能直接計算出對應的記憶體位址,而是需要根據指標乙個結點乙個結點地依次遍歷,直到找到相應的結點。所以鍊錶隨機訪問效能沒有陣列好,時間複雜度為o(n)。

迴圈鍊錶

迴圈鍊錶是一種特殊的單鏈表。它跟單鏈表唯一的區別就在尾結點。單鏈表的尾結點指標指向空位址,表示這就是最後的結點了。而迴圈鍊錶的尾結點指標是指向鍊錶的頭結點,它像乙個環一樣首尾相連,所以叫作迴圈鍊錶。

和單鏈表相比,迴圈鍊錶的優點是從鏈尾到鏈頭比較方便。當要處理的資料具有環型結構特點時,就特別適合採用迴圈鍊錶。比如著名的約瑟夫問題。儘管用單鏈表也可以實現,但是用迴圈鍊錶實現的話,**就會簡潔很多。

雙向鍊錶

單向鍊錶只有乙個方向,結點只有乙個後繼指標 next 指向後面的結點。而雙向鍊錶支援兩個方向,每個結點不止有乙個後繼指標 next 指向後面的結點,還有乙個前驅指標 prev 指向前面的結點。雙向鍊錶需要額外的兩個空間來儲存後繼結點和前驅結點的位址。所以,如果儲存同樣多的資料,雙向鍊錶要比單鏈表占用更多的記憶體空間。雖然兩個指標比較浪費儲存空間,但可以支援雙向遍歷,這樣也帶來了雙向鍊錶操作的靈活性。

從結構上來看,雙向鍊錶可以支援 o(1) 時間複雜度的情況下找到前驅結點,正是這樣的特點,也使雙向鍊錶在某些情況下的插入、刪除等操作都要比單鏈表簡單、高效。

在實際的軟體開發中,從鍊錶中刪除乙個資料有兩種情況:

刪除結點中「值等於菶給定值」的結點

不管是單鏈表還是雙向鍊錶,為了查詢到值等於給定值的結點,都需要從頭結點開始乙個乙個依次遍歷對比,直到找到值等於給定值的結點,然後再通過前面提到的指標操作將其刪除。 儘管單純的刪除操作時間複雜度是 o(1),但遍歷查詢的時間是主要的耗時點,對應的時間複雜度為 o(n)。根據時間複雜度分析中的加法法則,刪除值等於給定值的結點對應的鍊錶操作的總時間複雜度為 o(n)。

刪除給定指標指向的結點

已經找到了要刪除的結點,但是刪除某個結點 q 需要知道其前驅結點,而單鏈表並不支援直接獲取前驅結點,所以,為了找到前驅結點,我們還是要從頭結點開始遍歷鍊錶,直到 p->next=q,說明 p 是 q 的前驅結點。但是對於雙向鍊錶來說,這種情況就比較有優勢了。因為雙向鍊錶中的結點已經儲存了前驅結點的指標,不需要像單鏈表那樣遍歷。所以,單鏈表刪除操作需要 o(n) 的時間複雜度,而雙向鍊錶只需要在 o(1) 的時間複雜度內就搞定了。

同理,如果我們希望在鍊錶的某個指定結點前面插入乙個結點,雙向鍊錶比單鏈表有很大的優勢。雙向鍊錶可以在 o(1) 時間複雜度搞定,而單向鍊錶需要 o(n) 的時間複雜度。除了插入、刪除操作有優勢之外,對於乙個有序鍊錶,雙向鍊錶的按值查詢的效率也要比單鏈表高一些。因為,我們可以記錄上次查詢的位置 p,每次查詢時,根據要查詢的值與 p 的大小關係,決定是往前還是往後查詢,所以平均只需要查詢一半的資料。

用空間換時間的設計思想:當記憶體空間充足的時候,如果更加追求**的執行速度,我們就可以選擇空間複雜度相對較高、但時間複雜度相對很低的演算法或者資料結構。相反,如果記憶體比較緊缺,比如**跑在手機或者微控制器上,這個時候,就要反過來用時間換空間的設計思路。

雙向迴圈鍊錶

鍊錶vs陣列

時間複雜度

陣列鍊錶

插入刪除

o(n)

o(1)

隨機訪問

o(1)

o(n)

陣列簡單易用,在實現上使用的是連續的記憶體空間,可以借助 cpu 的快取機制,預讀陣列中的資料,所以訪問效率更高。而鍊錶在記憶體中並不是連續儲存,所以對 cpu 快取不友好,沒辦法有效預讀。陣列的缺點是大小固定,一經宣告就要占用整塊連續記憶體空間。如果宣告的陣列過大,系統可能沒有足夠的連續記憶體空間分配給它,導致「記憶體不足(out of memory)」。如果宣告的陣列過小,則可能出現不夠用的情況。這時只能再申請乙個更大的記憶體空間,把原陣列拷貝進去,非常費時。鍊錶本身沒有大小的限制,天然地支援動態擴容。

如何基於鍊錶實現 lru 快取淘汰演算法?

快取是一種提高資料讀取效能的技術,在硬體設計、軟體開發中都有著非常廣泛的應用,比如常見的 cpu 快取、資料庫快取、瀏覽器快取等等。快取的大小有限,當快取被用滿時,哪些資料應該被清理出去,哪些資料應該被保留?這就需要快取淘汰策略來決定。常見的策略有三種:先進先出策略 fifo(first in,first out)、最少使用策略 lfu(least frequently used)、最近最少使用策略 lru(least recently used)。

快取實際上就是利用了空間換時間的設計思想。如果把資料儲存在硬碟上,會比較節省記憶體,但每次查詢資料都要詢問一次硬碟,會比較慢。但如果通過快取技術,事先將資料載入在記憶體中,雖然會比較耗費記憶體空間,但是每次資料查詢的速度就大大提高了。

所以對於執行較慢的程式,可以通過消耗更多的記憶體(空間換時間)來進行優化;而消耗過多記憶體的程式,可以通過消耗更多的時間(時間換空間)來降低記憶體的消耗。

回到正題:思路:維護乙個有序單鏈表,越靠近鍊錶尾部的結點是越早之前訪問的。當有乙個新的資料被訪問時,我們從煉表頭開始順序遍歷鍊錶。

如果此資料之前已經被快取在鍊錶中了,我們遍歷得到這個資料對應的結點,並將其從原來的位置刪除,然後再插入到鍊錶的頭部。

如果此資料沒有在快取鍊錶中,又可以分為兩種情況:

如果此時快取未滿,則將此結點直接插入到鍊錶的頭部; - 如果此時快取未滿,則將此結點直接插入到鍊錶的頭部;如果此時快取已滿,則鍊錶尾結點刪除,將新的資料結點插入鍊錶的頭部。

cpu快取機制

cpu在從記憶體讀取資料的時候,會先把讀取到的資料載入到cpu的快取中。而cpu每次從記憶體讀取資料並不是只讀取那個特定要訪問的位址,而是讀取乙個資料塊並儲存到cpu快取中,然後下次訪問記憶體資料的時候就會先從cpu快取開始查詢,如果找到就不需要再從記憶體中取。這樣就實現了比記憶體訪問速度更快的機制,也就是cpu快取存在的意義:為了彌補記憶體訪問速度過慢與cpu執行速度快之間的差異而引入。

回文字串

由於回文串最重要的就是對稱,那麼最重要的問題就是找到那個中心,用快指標每步兩格走,當他到達鍊錶末端的時候,慢指標剛好到達中心,慢指標在過來的這趟路上還做了一件事,他把走過的節點反向了,在中心點再開闢乙個新的指標用於往回走,而慢指標繼續向前,當慢指標掃完整個鍊錶,就可以判斷這是回文串,否則就提前退出,總的來說時間複雜度按慢指標遍歷一遍來算是o(n),空間複雜度因為只開闢了3個額外的輔助,所以是o(1)

ps:本內容**極客時間資料結構與演算法之美課程。

資料結構學習筆記 鍊錶

表示式的計算 表示式的計算涉及到棧的操作 對於表示式 a b c d e f 演算法 用到兩個棧,分別是符號棧和運算元棧。輸入表示式時,為了表示表示式輸入完畢,在表示式的最後加上 號,也就是說輸入的表示式為 a b c d e f 首先設定各個符號的優先順序,和 的優先順序為0,也就是最低的 和 的...

資料結構學習筆記 鍊錶

2.建立鍊錶 3.單向和雙向迴圈鍊錶 4.總結 struct list node 首先了解鍊錶的組成部分 說明 頭節點 在單鏈表的第乙個結點之前附設乙個結點,它沒有直接前驅,稱之為頭結點,頭結點的資料域可以不儲存任何資訊,指標域指向第乙個節點 首節點 的位址。頭結點的作用是使所有鍊錶 包括空表 的頭...

資料結構學習 鍊錶

將從下面4部分進行介紹 首先介紹鍊錶是什麼,然後介紹為什麼定義鍊錶,接著是鍊錶的分類,最後簡單介紹一下鍊錶結點的插入與刪除方法。首先,在介紹鍊錶之前,我們先介紹一下什麼是順序儲存結構。我們知道資料在計算機中的儲存就像貨物在倉庫中的儲存一樣,不但占用一定的空間,還要有乙個標示儲存位置的位址。計算機通過...