1. 什麼是連結串列
連結串列是通過指標把一組零散的記憶體塊串聯在一起的線性資料結構。
連結串列和陣列的記憶體分佈如下圖所示:
可以看出,連結串列和陣列的最大區別在於,陣列需要一塊連續的記憶體空間來儲存,對記憶體的要求較高。而連結串列不需要連續的記憶體空間,它通過指標將一組零散的記憶體塊串聯起來使用。
根據指標的不同使用方式,連結串列又可以分為單連結串列、雙向連結串列和迴圈連結串列。
- 單連結串列
- 結點包括當前資料和後繼結點的地址
- 雙向連結串列
- 結點包括當前資料、前驅結點的地址和後繼結點的地址
- 迴圈連結串列
- 結點包括當前資料和後繼結點的地址,尾結點的指標指向頭結點
- 雙向迴圈連結串列
- 結點包括當前資料、前驅結點的地址和後繼結點的地址,尾結點的後繼結點是頭結點,頭結點的前驅結點是尾結點
2. 連結串列的基本操作及其複雜度
2.1 查詢
想要隨機訪問連結串列的第 k 個元素,就沒有陣列那麼高效了。因為連結串列中的資料並非連續儲存的,不能像陣列那樣,根據下標和首地址,通過定址公式就能直接計算出對應的記憶體地址。而是要根據指標一個結點一個結點的依次遍歷。因此,需要 O(n) 的時間複雜度。
特別的,對於雙向連結串列,給定一個結點,要找出其前驅結點時,時間複雜度為 O(1),單連結串列則仍為 O(n)。
2.2 插入、刪除
在前一篇中我們提到,進行陣列的插入、刪除操作時,為了保證記憶體的連續性,需要做大量的資料搬移,所以時間複雜度是 O(n)。
而在連結串列中插入、刪除時,因為不需要為了保持記憶體的連續性而搬移結點,所以是非常快速的,只需要 O(1) 的時間複雜度。
雖然連結串列的插入、刪除操作時間複雜度只要 O(1),但是,實際情況下卻並非如此。因為,在實際開發中,還需要定位到進行操作的位置。例如,在連結串列中刪除一個資料有可能是這兩種情況:
- 刪除結點中“值等於某個給定值”的結點
- 刪除給定指標指向的結點
對於第一種情況,需要對連結串列進行遍歷,找到相應的位置然後刪除,此時刪除操作的時間複雜度為 O(n)。
對於第二種情況,已知了要刪除的結點,但是刪除某個結點需要知道它的前驅結點,對於單連結串列仍然需要遍歷尋找,時間複雜度為 O(n);而對於雙向連結串列,可以直接找到,所以時間複雜度為 O(1)。這也是雙向連結串列在實際開發中經常使用的原因。
連結串列和陣列的比較
連結串列和陣列是兩種截然不同的記憶體組織方式,正因如此,它們插入、刪除、隨機訪問的時間複雜度正好相反。
陣列使用的是連續的記憶體空間,可以利用空間區域性性原理,藉助 CPU cache 進行預讀,所以訪問效率更高。而連結串列不是連續儲存,無法進行快取,隨機訪問效率也較低。
陣列的缺點是大小固定,一經宣告就要佔用整塊連續的記憶體空間。如果宣告的陣列過大,系統可能沒有足夠的連續記憶體空間用於分配,就會導致“記憶體不足(out of memory)”。而如果宣告的陣列過小,當不夠用時,又需要重新申請一塊更大的記憶體,然後進行資料拷貝,非常費時。
而連結串列則沒有大小限制,支援動態擴容。當然,因為連結串列中每個結點都需要儲存前驅 / 後繼結點的指標,所以記憶體消耗會翻倍。而且,對連結串列頻繁的插入、刪除操作會導致頻繁的記憶體申請和釋放,容易造成記憶體碎片和觸發垃圾回收(Garbage Collection, GC)。
本文是《資料結構與演算法之美》的讀書筆記,首發於公眾號《程式碼寫完了》