連結串列(上)—— LRU 快取淘汰演算法的實現
經典的連結串列應用場景就是 LRU 快取淘汰演算法。
1. 連結串列結構
陣列需要一塊連續的記憶體空間來儲存,對記憶體的要求比較高。而連結串列不需要,它通過“ 指標 ”將一組零散的記憶體塊串聯起來使用。
三種常見的連結串列結構: 單鏈表 、 雙向連結串列 和 迴圈連結串列 。
單鏈表:頭結點記錄連結串列的基地址,可以用來遍歷整條連結串列。尾結點的指標指向空地址 NULL,表示最後的結點。每個結點儲存資料 data 和後繼指標 next,如下:

針對連結串列的插入和刪除操作,只需要考慮相鄰結點的指標改變,所以對應的時間複雜度是 O(1)。但是,隨機訪問需要從頭結點開始遍歷,所以時間複雜度是 O(n)。
迴圈連結串列是一種特殊的單鏈表,它的尾結點指標指向連結串列的頭結點,優點是從鏈尾到鏈頭比較方便。當要處理的資料具有環型結構特點時,就特別適合採用迴圈連結串列,比如約瑟夫問題。

雙向連結串列支援兩個方向,每個結點有個後繼指標 next 指向後面的結點和一個前驅指標 prev 指向前面的結點。它支援雙向遍歷,帶來了操作的靈活性。雙向連結串列可以支援 O(1) 時間複雜度的情況下找到前驅結點,這使得它在某些情況下的插入、刪除等操作比單鏈表簡單高效。對於一個有序連結串列,雙向連結串列的按值查詢的效率也要比單鏈表高一些。
在實際的軟體開發中,雙向連結串列儘管比較費記憶體,但比單鏈表的應用更加廣泛。Java 語言中的 LinkedHashMap 就用到了雙向連結串列,這是用空間換時間的設計思想。

2. 連結串列、陣列效能比較
時間複雜度 | 陣列 | 連結串列 |
---|---|---|
插入、刪除 | O(n) | O(1) |
隨機訪問 | O(1) | O(n) |
陣列簡單易用,在實現上使用的是連續的記憶體空間,可以藉助 CPU 的快取機制,預讀陣列中的資料,所以訪問效率更高。而連結串列在記憶體中並不是連續儲存,所以對 CPU 快取不友好,沒辦法有效預讀。(此處是區域性性原理)
陣列的缺點是大小固定,要佔用整塊連續記憶體空間。如果陣列過大,容易導致 OOM。擴容時需要拷貝陣列,非常耗時。連結串列本身沒有大小的限制,天然地支援動態擴容。
如果程式碼對記憶體的使用非常苛刻,那陣列就是更適合的選擇。連結串列需要額外儲存指標結點,頻繁的增刪操作容易造成記憶體碎片,如果用 Java 語言,就可能導致頻繁 GC。
如何用連結串列實現 LRU 快取呢?
維護一個有序單鏈表,靠近尾部的結點是最早訪問的。當有資料被訪問時,從頭開始遍歷連結串列。
- 如果資料被快取過,遍歷得到對應的結點,把它從原來位置刪除,插入到連結串列頭部。
- 如果沒有快取過,那麼分兩種情況:
- 如果快取沒滿,那麼直接把新資料插入連結串列尾部;
- 如果快取已滿,那麼把尾結點刪除,新資料插入連結串列頭部。
基於連結串列的實現思路,快取訪問的時間複雜度為 O(n)。考慮一下優化,比如引入散列表老記錄每個資料的位置,使訪問時間複雜度降到 O(1)。
思考題:
如何判斷一個字串是否是迴文字串?如果字串是通過單鏈表來儲存的,那該如何來判斷是一個迴文串呢?
自己是從事了七年開發的Android工程師,不少人私下問我,2019年Android進階該怎麼學,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發,卻又不知道怎麼進階學習的朋友。【 包括高階UI、效能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料 】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。
資料獲取方式:加入Android架構交流QQ群聊:513088520 ,進群即領取資料!!!
點選連結加入群聊【Android移動架構總群】:加入群聊

資料大全