一、什麼是鍊錶?
和陣列一樣,鍊錶也是一種線性表。
從記憶體結構來看,鍊錶的記憶體結構是不連續的記憶體空間,是將一組零散的記憶體塊串聯起來,從而進行資料儲存的資料結構。
二、為什麼使用鍊錶?即鍊錶的特點
三、常用鍊錶:單鏈表、迴圈鍊錶和雙向鍊錶
四、選擇陣列還是鍊錶?
五、應用
如何分別用鍊錶和陣列實現lru緩衝淘汰策略?
什麼是快取?
快取是一種提高資料讀取效能的技術,在硬體設計、軟體開發中都有著非廣泛的應用,比如常見的cpu快取、資料庫快取、瀏覽器快取等等。
為什麼使用快取?即快取的特點
快取的大小是有限的,當快取被用滿時,哪些資料應該被清理出去,哪些資料應該被保留?就需要用到快取淘汰策略。
什麼是快取淘汰策略?
指的是當快取被用滿時清理資料的優先順序。
有哪些快取淘汰策略?
常見的3種包括先進先出策略fifo(first in,first out)、最少使用策略lfu(least frequently used)、最近最少使用策略lru(least recently used)。
鍊錶實現lru快取淘汰策略
當訪問的資料沒有儲存在快取的鍊錶中時,直接將資料插入煉表表頭,時間複雜度為o(1);當訪問的資料存在於儲存的鍊錶中時,將該資料對應的節點,插入到煉表表頭,時間複雜度為o(n)。如果快取被佔滿,則從鍊錶尾部的資料開始清理,時間複雜度為o(1)。
陣列實現lru快取淘汰策略
方式一:首位置儲存最新訪問資料,末尾位置優先清理
當訪問的資料未存在於快取的陣列中時,直接將資料插入陣列第乙個元素位置,此時陣列所有元素需要向後移動1個位置,時間複雜度為o(n);當訪問的資料存在於快取的陣列中時,查詢到資料並將其插入陣列的第乙個位置,此時亦需移動陣列元素,時間複雜度為o(n)。快取用滿時,則清理掉末尾的資料,且剩餘陣列元素需整體後移一位,時間複雜度為o(n)。
方式二:首位置優先清理,末尾位置儲存最新訪問資料
當訪問的資料未存在於快取的陣列中時,直接將資料新增進陣列作為當前最後乙個元素時間複雜度為o(1);當訪問的資料存在於快取的陣列中時,查詢到資料並將其插入當前陣列最後乙個元素的位置,此時亦需移動陣列元素,時間複雜度為o(n)。快取用滿時,則清理掉陣列首位置的元素,且剩餘陣列元素需整體前移一位,時間複雜度為o(n)。(優化:清理的時候可以考慮一次性清理一定數量,從而降低清理次數,提高效能。)
六、設計思想
時空替換思想:「用空間換時間」 與 「用時間換空間」
當記憶體空間充足的時候,如果我們更加追求**的執行速度,我們就可以選擇空間複雜度相對較高,時間複雜度小相對較低的演算法和資料結構,快取就是空間換時間的例子。如果記憶體比較緊缺,比如**跑在手機或者微控制器上,這時,就要反過來用時間換空間的思路。
七、課後習題
如何判斷乙個字串是否是回文字串的問題,我想你應該聽過,我們今天的題目就是基於這個問題的改造版本。如果字串是通過單鏈表來儲存的,那該如何來判斷是乙個回文串呢?你有什麼好的解決思路呢?相應的時間空間複雜度又是多少呢?
方法一:半棧法
用快慢兩個指標遍歷,同時用棧copy慢指標指向的data。
完成後,慢指標指向中間節點,耗時為n/2.
最後用pop棧中的data和慢指標指向的data比較,耗時也是n/2.
所以時間複雜度為:o(n),空間複雜度因棧額外儲存了一半的data,故為o(n/2)
方法二:全棧法
全部遍歷,data壓棧,額外空間消耗n
再次全部遍歷取data,同時pop棧取data, 二者比較,時間消耗2n。
所以時間複雜度為o(3n),空間複雜度為o(n)。
該法演算法最簡單,但複雜度高。可以用棧儲存節點指標,而非data來改進。
方法三:硬幹法
乙個指標從頭取data,另乙個指標遍歷到底取data,比較二者,刪除尾部節點,重複1。
時間複雜度高達 o(n^2),空間複雜度卻最低o(1)。
/**
* definition for singly-linked list.
* public class listnode
* }*/class solution
listnode prev = null;
listnode slow = head;
listnode fast = head;
while (fast != null && fast.next != null)
if (fast != null)
while (slow != null)
slow = slow.next;
prev = prev.next;
}return true;
}}
一、理解指標或引用的含義
示例:p—>next = q; 表示p節點的後繼指標儲存了q節點的記憶體位址。
p—>next = p—>next—>next; 表示p節點的後繼指標儲存了p節點的下下個節點的記憶體位址。
二、警惕指標丟失和記憶體洩漏(單鏈表)
在插入和刪除結點時,要注意先持有後面的結點再操作,否者一旦後面結點的前繼指標被斷開,就無法再訪 問,導致記憶體洩漏。
插入節點
在節點a和節點b之間插入節點x,b是a的下一節點,,p指標指向節點a,則造成指標丟失和記憶體洩漏的**:p—>next = x;x—>next = p—>next; 顯然這會導致x節點的後繼指標指向自身。
正確的寫法是2句**交換順序,即:x—>next = p—>next; p—>next = x;
三、利用「哨兵」簡化實現難度
鍊錶的插入、刪除操作,需要對插入第乙個結點和刪除最後乙個節點做特殊處理。利用哨兵物件可以不用邊界判斷,鍊錶的哨兵物件是只存指標不存資料的頭結點。
什麼是「哨兵」?
鍊錶中的「哨兵」節點是解決邊界問題的,不參與業務邏輯。如果我們引入「哨兵」節點,則不管鍊錶是否為空,head指標都會指向這個「哨兵」節點。我們把這種有「哨兵」節點的鍊錶稱為帶頭鍊錶,相反,沒有「哨兵」節點的鍊錶就稱為不帶頭鍊錶。
未引入「哨兵」的情況
如果在p節點後插入乙個節點,只需2行**即可搞定:
new_node—>next = p—>next;
p—>next = new_node;
但,若向空煉表中插入乙個節點,則**如下:
if(head == null)
如果要刪除節點p的後繼節點,只需1行**即可搞定:
p—>next = p—>next—>next;
但,若是刪除鍊錶的最後乙個節點(鍊錶中只剩下這個節點),則**如下:
if(head—>next == null)
從上面的情況可以看出,針對鍊錶的插入、刪除操作,需要對插入第乙個節點和刪除最後乙個節點的情況進行特殊處理。這樣**就會顯得很繁瑣,所以引入「哨兵」節點來解決這個問題。
引入「哨兵」的情況
「哨兵」節點不儲存資料,無論鍊錶是否為空,head指標都會指向它,作為鍊錶的頭結點始終存在。這樣,插入第乙個節點和插入其他節點,刪除最後乙個節點和刪除其他節點都可以統一為相同的**實現邏輯了。
四、重點留意邊界條件處理
經常用來檢查鍊錶是否正確的邊界4個邊界條件:
如果鍊錶為空時
如果鍊錶只包含乙個節點時
如果鍊錶只包含兩個節點時
**邏輯在處理頭尾節點時
五、舉例畫圖,輔助思考
六、多寫多練,沒有捷徑
5個常見的鍊錶操作:
單鏈表反轉
鍊錶中環的檢測
兩個有序鍊錶合併
刪除鍊錶倒數第n個節點
求鍊錶的中間節點
練習題leetcode對應編號:206,141,21,19,876
哨兵**優勢: **2為什麼比**1效能更優? 為什麼**2少了乙個比較?
原因如下,首先要明確作者舉兩個**例子的目的是為了說明"哨兵"的優勢.
我們先分析沒有哨兵的**1,邏輯很簡單,在遍歷陣列的時候,挨個比較每乙個元素是否等於key,另外我們要還判斷迴圈條件i是否小於n,如果相等了,那麼就退出迴圈遍歷,所以針對每乙個元素判斷都進行了2次比較.
**2,一開始就把陣列中最後乙個元素修改成了目標key,while一次迴圈中,迴圈條件僅僅判斷當前陣列元素是否等於key,對於跳出迴圈的條件是省略的,為什麼呢?因為前面說了,陣列最後乙個元素改成了key,最後肯定會在陣列中找到key,也就是定會跳出. 於是最後我們只關注i是不是n-1就可以了,是n-1代表"原始整個陣列"元素中的確沒有key.
鍊錶 如何實現LRU快取淘汰演算法
乙個經典的鍊錶應用場景,就是lru快取淘汰演算法 快取是一種提高資料讀取效能的技術,比如cpu快取 資料庫快取 瀏覽器快取等等 快取的大小有限,當快取被用滿的時候哪些資料該被清理出去?哪些資料該被保留?需要快取淘汰策略來決定 先進先出策略fifo,最少使用策略lfu,最近最少使用策略lru 陣列需要...
鍊錶(上) 如何實現LRU快取淘汰演算法
鍊錶是一種最基礎的資料結構,學習鍊錶有什麼用?為了回答這個問題,先來討論乙個經典的鍊錶應用場景,那就是 lru 快取淘汰演算法。快取是一種提高資料讀取效能的技術,在硬體設計 軟體開發中都有著非常廣泛的應用,比如常見的 cpu 快取 資料庫快取 瀏覽器快取等等。快取的大小有限,當快取被用滿時,哪些資料...
06 鍊錶(上) 如何實現LRU快取淘汰演算法
我們先來討論乙個經典的鍊錶應用場景,那就是 lru 快取淘汰演算法。快取的大小有限,當快取被用滿時,哪些資料應該被清理出去,哪些資料應該被保留?這就需要快取淘汰策略來決定。常見的策略有三種 先進先出策略 fifo first in,first out 最少使用策略 lfu least frequen...