1. 程式人生 > >資料結構與算法系列5--連結串列

資料結構與算法系列5--連結串列

什麼是連結串列?

1.和陣列一樣,連結串列也是一種線性表。
2.從記憶體結構來看,連結串列的記憶體結構是不連續的記憶體空間,是將一組零散的記憶體塊串聯起來,從而進行資料儲存的資料結構。
3.連結串列中的每一個記憶體塊被稱為節點Node。節點除了儲存資料外,還需記錄鏈上下一個節點的地址,即後繼指標next。

常用連結串列:

1.單鏈表
1)每個節點只包含一個指標,即後繼指標。
2)單鏈表有兩個特殊的節點,即首節點和尾節點。為什麼特殊?用首節點地址表示整條連結串列,尾節點的後繼指標指向空地址null。
3)效能特點:插入和刪除節點的時間複雜度為O(1),查詢的時間複雜度為O(n)。
2.迴圈連結串列


1)除了尾節點的後繼指標指向首節點的地址外均與單鏈表一致。
2)適用於儲存有迴圈特點的資料,比如約瑟夫問題。
3.雙向連結串列
1)節點除了儲存資料外,還有兩個指標分別指向前一個節點地址(前驅指標prev)和下一個節點地址(後繼指標next)。
2)首節點的前驅指標prev和尾節點的後繼指標均指向空地址。
3)效能特點:
和單鏈表相比,儲存相同的資料,需要消耗更多的儲存空間。
插入、刪除操作比單鏈表效率更高O(1)級別。以刪除操作為例,刪除操作分為2種情況:給定資料值刪除對應節點和給定節點地址刪除節點。對於前一種情況,單鏈表和雙向連結串列都需要從頭到尾進行遍歷從而找到對應節點進行刪除,時間複雜度為O(n)。對於第二種情況,要進行刪除操作必須找到前驅節點,單鏈表需要從頭到尾進行遍歷直到p->next = q,時間複雜度為O(n),而雙向連結串列可以直接找到前驅節點,時間複雜度為O(1)。
對於一個有序連結串列,雙向連結串列的按值查詢效率要比單鏈表高一些。因為我們可以記錄上次查詢的位置p,每一次查詢時,根據要查詢的值與p的大小關係,決定是往前還是往後查詢,所以平均只需要查詢一半的資料。

連結串列的特點

1.插入、刪除資料效率高O(1)級別(只需更改指標指向即可),隨機訪問效率低O(n)級別(需要從鏈頭至鏈尾進行遍歷)。
2.和陣列相比,記憶體空間消耗更大,因為每個儲存資料的節點都需要額外的空間儲存後繼指標。

連結串列和陣列比較?

陣列:插入、刪除的時間複雜度是O(n),隨機訪問的時間複雜度是O(1)。
連結串列:插入、刪除的時間複雜度是O(1),隨機訪問的時間複雜端是O(n)。
陣列缺點:
1)若申請記憶體空間很大,比如100M,但若記憶體空間沒有100M的連續空間時,則會申請失敗,儘管記憶體可用空間超過100M。
2)大小固定,若儲存空間不足,需進行擴容,一旦擴容就要進行資料複製,而這時非常費時的。
連結串列缺點


1)記憶體空間消耗更大,因為需要額外的空間儲存指標資訊。
2)對連結串列進行頻繁的插入和刪除操作,會導致頻繁的記憶體申請和釋放,容易造成記憶體碎片,如果是Java語言,還可能會造成頻繁的GC(自動垃圾回收器)操作。

應用(實現LRU緩衝淘汰策略)

1)什麼是快取?
快取是一種提高資料讀取效能的技術,在硬體設計、軟體開發中都有著非廣泛的應用,比如常見的CPU快取、資料庫快取、瀏覽器快取等等。
2)為什麼使用快取?即快取的特點
快取的大小是有限的,當快取被用滿時,哪些資料應該被清理出去,哪些資料應該被保留?就需要用到快取淘汰策略。
3)什麼是快取淘汰策略?
指的是當快取被用滿時清理資料的優先順序。
4)有哪些快取淘汰策略?
常見的3種包括先進先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frenquently Used)、最近最少使用策略LRU(Least Recently Used)。
5)連結串列實現LRU快取淘汰策略?
當訪問的資料存在於儲存的連結串列中時,將該資料對應的節點,插入到連結串列表頭,時間複雜度為O(n)。如果快取被佔滿,則從連結串列尾部的資料開始清理,時間複雜度為O(1)。

設計思想

時空替換思想:“用空間換時間” 與 “用時間換空間”
當記憶體空間充足的時候,如果我們更加追求程式碼的執行速度,我們就可以選擇空間複雜度相對較高,時間複雜度小相對較低的演算法和資料結構,快取就是空間換時間的例子。如果記憶體比較緊缺,比如程式碼跑在手機或者微控制器上,這時,就要反過來用時間換空間的思路。

連結串列學習的幾個技巧

一、理解指標或引用的含義
1.含義:將某個變數(物件)賦值給指標(引用),實際上就是就是將這個變數(物件)的地址賦值給指標(引用)。
2.示例:
p—>next = q; 表示p節點的後繼指標儲存了q節點的記憶體地址。
p—>next = p—>next—>next; 表示p節點的後繼指標儲存了p節點的下下個節點的記憶體地址。
二、警惕指標丟失和記憶體洩漏(單鏈表)
1.插入節點
在節點a和節點b之間插入節點x,b是a的下一節點,,p指標指向節點a,則造成指標丟失和記憶體洩漏的程式碼:p—>next = x;x—>next = p—>next; 顯然這會導致x節點的後繼指標指向自身。
正確的寫法是2句程式碼交換順序,即:x—>next = p—>next; p—>next = x;
2.刪除節點
在節點a和節點b之間刪除節點b,b是a的下一節點,p指標指向節點a:p—>next = p—>next—>next;
三、利用“哨兵”簡化實現難度
1.什麼是“哨兵”?
連結串列中的“哨兵”節點是解決邊界問題的,不參與業務邏輯。如果我們引入“哨兵”節點,則不管連結串列是否為空,head指標都會指向這個“哨兵”節點。我們把這種有“哨兵”節點的連結串列稱為帶頭連結串列,相反,沒有“哨兵”節點的連結串列就稱為不帶頭連結串列。
2.未引入“哨兵”的情況
如果在p節點後插入一個節點,只需2行程式碼即可搞定:
new_node—>next = p—>next;
p—>next = new_node;
但,若向空連結串列中插入一個節點,則程式碼如下:
if(head == null){
head = new_node;
}
如果要刪除節點p的後繼節點,只需1行程式碼即可搞定:
p—>next = p—>next—>next;
但,若是刪除連結串列的最有一個節點(連結串列中只剩下這個節點),則程式碼如下:
if(head—>next == null){
head = null;
}
從上面的情況可以看出,針對連結串列的插入、刪除操作,需要對插入第一個節點和刪除最後一個節點的情況進行特殊處理。這樣程式碼就會顯得很繁瑣,所以引入“哨兵”節點來解決這個問題。
3.引入“哨兵”的情況
“哨兵”節點不儲存資料,無論連結串列是否為空,head指標都會指向它,作為連結串列的頭結點始終存在。這樣,插入第一個節點和插入其他節點,刪除最後一個節點和刪除其他節點都可以統一為相同的程式碼實現邏輯了。
四、重點留意邊界條件處理
經常用來檢查連結串列是否正確的邊界4個邊界條件:
1.如果連結串列為空時,程式碼是否能正常工作?
2.如果連結串列只包含一個節點時,程式碼是否能正常工作?
3.如果連結串列只包含兩個節點時,程式碼是否能正常工作?
4.程式碼邏輯在處理頭尾節點時是否能正常工作?

5個常見的連結串列操作:

1.單鏈表反轉
2.連結串列中環的檢測
3.兩個有序連結串列合併
4.刪除連結串列倒數第n個節點
5.求連結串列的中間節點