1. 程式人生 > >連結串列(上):如何實現LRU快取淘汰演算法?

連結串列(上):如何實現LRU快取淘汰演算法?

本文是學習演算法的筆記,《資料結構與演算法之美》,極客時間的課程

連結串列(Linked list)

快取技術是一種提高資料讀取效能的技術,應用廣泛。快取的大小有限,當快取被用滿的時候,哪些資料應該被保留?這需要快取淘汰策略來決定。

常見的策略有三種:

先進先出策略FIFO(First In First Out)

最少使用策略LFU(Least Frequently Used)

最近最少使用策略LRU(Least Recently Used)

回到今天的正題——如何用連結串列來實現LRU快取淘汰策略

與陣列相比,連結串列是稍微複雜一點為的資料結構。連結串列不需要連續的記憶體空間,它通過“指標”,將一組零散的記憶體塊串聯起來使用。其中我們把記憶體塊稱為連結串列的“結點”。

連結串列的結構五花八門,這裡介紹三種常見的連結串列結構————單鏈表、雙向連結串列和迴圈連結串列。

單鏈表

每個連結串列的結點,除了儲存資料之外,還需要記錄下一個結點的地址。我們把這個記錄下一個結點地址的指標叫作後繼指標next。
在這裡插入圖片描述
第一個節點通常叫頭結點,記錄連結串列的基地址,有了它,就可以遍歷整個連結串列。

最後一個節點叫作尾節點,尾節點的指標不是指向下一個節點,而是指向一個空地址NULL。表示這是連結串列最後一個結點。

與陣列一樣,連結串列支援查詢、插入和刪除操作。

我們知道,陣列進行插入、刪除操作,為了保持記憶體的連續性,需要進行大量的資料搬移,所以時間複雜度是O(n)。而連結串列進行插入和刪除,只考慮相鄰節點指標的改變,時間複雜度是O(1)。
在這裡插入圖片描述


但連結串列要想隨機訪問第k個元素,就沒有陣列高效了。要通過指標,從首節點開始,一個一個結點的遍歷,直到找到相應的結點。時間複雜度是O(n)。

迴圈連結串列

單鏈表的尾節點指標指向空地址,表示這是最後 的結點。而迴圈列表的尾節點的指標指向連結串列的頭結點,從而成為一個迴圈結構,所以叫“迴圈連結串列”

雙向連結串列

單向連結串列只有一個方向,結點只有一個後繼指標next指向後面的結點,而雙向連結串列中,每個結點,還有一個前驅指標 prev 指向前面的結點。兩個指標就要佔用更多的空間,但可以支援雙向遍歷,也帶來操作的靈活性。
在這裡插入圖片描述
雙向連結串列支援在複雜度為O(1)的情況下找到前驅節點,這使得其刪除和插入操作,在某些情況下,比單向連結串列更簡潔、高效。

你在這兒可能就有疑問了,單向連結串列刪除和插入的時間複雜度是O(1),還能怎麼簡潔呢?

其實上面的說法是有前提的。這裡先具體分析下刪除操作。

刪除無外乎有兩種情況:1、刪除結點中“值等於某個定值”的結點。2、刪除給定指標指向的結點。

對於第一種情況,單向連結串列和雙向連結串列,都得從頭開始遍歷,找到給定值的節點,將其刪除。刪除的時間複雜度是O(1),但遍歷查詢是主要耗時點,時間複雜度是O(n),刪除值等於給定節點對應的連結串列操作總的時間複雜度是O(n)。

對第二種情況,已經找到的要刪除的節點,要刪除這個節點,需要知道它的前驅節點。單向連結串列就需要從頭遍歷得到前驅節點,時間複雜度為O(n),而雙向連結串列有前驅指標,可以在時間複雜度O(1)的情況下搞定。

插入操作的分析,也是這麼一個思路。

java語言中,LinkedHashMap的實現原理,就用到了雙向連結串列的資料結構。

如果迴圈連結串列和雙向連結串列結合在一起,就是雙向迴圈連結串列。

連結串列與陣列的比較

陣列和連結串列是兩種截然不同的記憶體組織方式,因為記憶體儲存的區別,它們插入、刪除、隨機訪問操作的時間複雜度正好相反。
在這裡插入圖片描述
但在實際的開發中,不能僅僅利用複雜度分析就決定使用哪個資料結構來儲存資料。比如陣列簡單易用,使用的連續記憶體空間,可以藉助CPU的快取機制,預讀陣列中的資料,訪問效率更高。而連結串列對CPU快取不友好,沒辦法預讀。陣列的缺點也是這個,若沒有足夠的連續記憶體分配給它,導致“記憶體不足”。

如果你的程式碼對記憶體的使用非常苛刻,那陣列更合適。因數連結串列中的每個指標需要額外的儲存空間。而且對連結串列進行頻繁的插入、刪除操作,還會導致頻繁的記憶體申請和釋放,容易造成記憶體碎片。如果是java語言,可能導致頻繁的GC。

所以,在實際的開發中,針對不同的型別的專案,要根據具體的尾部,權衡是選擇陣列還是連結串列。

最後,我們再說說開篇的那個問題,怎樣基於連結串列實現LRU淘汰演算法

我們的思路是這樣的,維護一個有序單鏈表,越靠近尾部的資料,就是越早訪問的。當有新資料被訪問時,從頭遍歷連結串列
1、資料已經在連結串列裡了,那先刪除其對應的節點,再把資料插入到連結串列頭部
2、資料不在列表裡,那直接在頭部插入該資料,若插入時,連結串列是滿的,把尾部的那個資料刪除了再插入。

至於這個思路的優化,以後再說