1. 程式人生 > >鏈表(上)

鏈表(上)

軟件開發 連續 鏈表 習慣性 null 特殊 訪問 局限 無法

鏈表(上)

@(數據結構與算法)

鏈表的經典應用場景: LRU 緩存淘汰算法。

緩存是一種提高數據讀取性能的計數,如常見的:CPU 緩存,數據庫緩存,瀏覽器緩存等。

緩存的大小有限,當緩存被用滿時,那些數據應該被清理出去,那些數據應該保留,這就需要緩存淘汰策略算法來決定。常見得策略有三種:先進先出策略 FIFO(First In ,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU (Least Recently Used)。

鏈表的結構

從底層的存儲數據結構上看,數組需要連續的內存空間來存儲,對內存的要求比較高,而鏈表並不需要一塊連續的內存空間,它通過“指針”將一組零散的內存塊串聯起來使用。

單鏈表有兩個節點是特殊的,他們分別是第一個節點和最後一個節點,習慣性稱之為頭結點和尾節點,頭結點用來記錄鏈表的基地址,而尾節點特殊的地方是:指針不是指向下一個節點,而是指向一個空地址 NULL。
技術分享圖片

在鏈表中插入或者刪除一個數據不需要像數組那樣為了保存內存的連續性而搬移結點,所以在鏈表中插入和刪除操作的時間復雜度為 $O(1)$。
技術分享圖片

但是有利就有弊,鏈表藥性隨機訪問第 k 個元素怒,就沒有數組那麽高效了,因為鏈表中的數據並非連續存儲,所以無法像數組那樣,根據首地址和下標,通過尋址公式就能直接計算出對應的內存地址,而是需要根據指針一個接一個結點的依次遍歷,直到找到相應的結點。時間復雜度為$O(n)$。

循環鏈表是一種特殊的單鏈表,唯一區別就在為節點指針指向鏈表的頭結點。
技術分享圖片

雙向鏈表,每個結點不止有一個後繼指針 next 指向後面的結點,還有一個前驅指針 prev 指向前面的結點,顯然需要更占用內存。
技術分享圖片

在實際的刪除操作中,無外乎這兩種情況

  • 刪除結點中“值等於某個給定值”的結點
  • 刪除給定指針指向的結點

對於第一種情況,無論單鏈表還是雙鏈表,都需要從頭結點遍歷一遍整個鏈表,直到找到值等於給定值的結點,將其刪除,盡管刪除的時間復雜度為 $O(1)$,但查找的時間復雜度為 $O(n)$,所以總的時間復雜度為 $O(n)$。

對於第二種情況,我們已經找到了要刪除的結點,但是刪除某個結點 q 需要知道其前驅結點,而單鏈表並不支持直接獲取前驅結點,所以還是需要從頭結點遍歷,直到找到 p->next = q,才可進行刪除,時間復雜度為 $O(n)$,而對於雙向鏈表,澤科直接進行刪除,時間復雜度為 $O(1)$。

同理如果我們希望在鏈表的某個指定結點前插入一個結點插入操作,雙向鏈表可以在 $O(1)$ 時間復雜度內搞定,而單鏈表則需要 $O(n)$ 時間復雜度。

雙向鏈表的重要思想是空間換時間,當內存空間充足時,如果更加追求代碼的執行速度,可以選擇空間復雜度相對較高,但時間復雜度相對很低的算法或者數據結構,相反同理。而緩存正是利用了空間換時間的設計思想。

鏈表和數組的比較

技術分享圖片

不過,數組和鏈表的對比,並不能局限於時間復雜度,而且,在實際的軟件開發中,不能僅僅利用復雜度分析就決定使用哪個數據結構來存儲數據。

數組簡單易用,在實現上使用的連續的內存空間,可以借助 CPU 的緩存機制,預讀數據,所以訪問效率更高。而鏈表在內存中並不是連續存儲的,所以不支持預讀。

數組聲明需要預先分配內存大小,而鏈表天然支持動態擴容。除此之外,如果代碼對內存的使用非常苛刻,數組更加適合,因為鏈表中的每個結點都需要消耗額外的存儲空間,而且,對鏈表進行頻繁的摻入、刪除操作,還會導致頻繁的內存申請和釋放,容易造成內存內存碎片。

鏈表實現 LRU 緩存淘汰算法

維護一個有序的單鏈表,越靠近鏈表的尾部的結點越早之前訪問,當有一個新的數據被訪問時,我們從鏈頭開始順序遍歷鏈表。

  1. 如果此數據之前已經被緩存在鏈表中,遍歷得到這個數據對應的結點,並將其從原來的位置刪除,然後再插入到鏈表的頭部。
  2. 如果此數據沒有在緩存鏈表中,兩種情況
  • 此時緩存未滿,則將此節點直接插入到鏈表的頭部。
  • 此時緩存已滿,則鏈表尾結點刪除,將新的數據節點插入鏈表頭部。

參考自:極客時間《數據結構與算法之美 》專欄

鏈表(上)