好吧,有人可能覺得我標題黨了,但我想告訴你們的是,前陣子面試確實掛在了 rlu 快取演算法的設計上了。當時做題的時候,自己想的太多了,感覺設計乙個 lru(least recently used) 快取演算法,不會這麼簡單啊,於是理解錯了題意(我也是服了,還能理解成這樣,,,,),自己一波操作寫了好多**,後來卡住了,再去仔細看題,發現自己應該是理解錯了,就是這麼簡單,設計乙個 lru 快取演算法。
不過這時時間就很緊了,按道理如果你真的對這個演算法很熟,十分鐘就能寫出來了,但是,自己雖然理解 lru 快取演算法的思想,也知道具體步驟,但之前卻從來沒有去動手寫過,導致在寫的時候,非常不熟練,也就是說,你感覺自己會 和你能夠用**完美著寫出來是完全不是一回事,所以在此提醒各位,如果可以,一定要自己用**實現一遍自己自以為會的東西。千萬不要覺得自己理解了思想,就不用去寫**了,獨自擼一遍**,才是真的理解了。
今天我帶大家用**來實現一遍 lru 快取演算法,以後你在遇到這型別的題,保證你完美秒殺它。
設計並實現最不經常使用(lfu)快取的資料結構。它應該支援以下操作:get 和 put。
get(key) - 如果鍵存在於快取中,則獲取鍵的值(總是正數),否則返回 -1。
put(key, value) - 如果鍵不存在,請設定或插入值。當快取達到其容量時,它應該在插入新專案之前,
使最不經常使用的專案無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,
最近最少使用的鍵將被去除。
高階:
你是否可以在 o(1) 時間複雜度內執行兩項操作?
示例:
lfucache cache = new lfucache( 2 /* capacity (快取容量) */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 去除 key 2
cache.get(2); // 返回 -1 (未找到key 2)
cache.get(3); // 返回 3
cache.put(4, 4); // 去除 key 1
cache.get(1); // 返回 -1 (未找到 key 1)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
我們要刪的是最近最少使用的節點,一種比較容易想到的方法就是使用單鏈表這種資料結構來儲存了。當我們進行 put 操作的時候,會出現以下幾種情況:
1、如果要 put(key,value) 已經存在於鍊錶之中了(根據key來判斷),那麼我們需要把鍊錶中久的資料刪除,然後把新的資料插入到鍊錶的頭部。、
2、如果要 put(key,value) 的資料沒有存在於鍊錶之後,我們我們需要判斷下快取區是否已滿,如果滿的話,則把鍊錶尾部的節點刪除,之後把新的資料插入到鍊錶頭部。如果沒有滿的話,直接把資料插入鍊錶頭部即可。
對於 get 操作,則會出現以下情況
1、如果要 get(key) 的資料存在於鍊錶中,則把 value 返回,並且把該節點刪除,刪除之後把它插入到鍊錶的頭部。
2、如果要 get(key) 的資料不存在於鍊錶之後,則直接返回 -1 即可。
時間、空間複雜度分析
對於這種方法,put 和 get 都需要遍歷鍊錶查詢資料是否存在,所以時間複雜度為 o(n)。空間複雜度為 o(1)。
在實際的應用中,當我們要去讀取乙個資料的時候,會先判斷該資料是否存在於快取器中,如果存在,則返回,如果不存在,則去別的地方查詢該資料(例如磁碟),找到後在把該資料存放於快取器中,在返回。
所以在實際的應用中,put 操作一般伴隨著 get 操作,也就是說,get 操作的次數是比較多的,而且命中率也是相對比較高的,進而 put 操作的次數是比較少的,我們我們是可以考慮採用空間換時間的方式來加快我們的 get 的操作的。
例如我們可以用乙個額外雜湊表(例如hashmap)來存放 key-value,這樣的話,我們的 get 操作就可以在 o(1) 的時間內尋找到目標節點,並且把 value 返回了。
然而,大家想一下,用了雜湊表之後,get 操作真的能夠在 o(1) 時間內完成嗎?
用了雜湊表之後,雖然我們能夠在 o(1) 時間內找到目標元素,可以,我們還需要刪除該元素,並且把該元素插入到鍊錶頭部啊,刪除乙個元素,我們是需要定位到這個元素的前驅的,然後定位到這個元素的前驅,是需要 o(n) 時間複雜度的。
最後的結果是,用了雜湊表時候,最壞時間複雜度還是 o(1),而空間複雜度也變為了 o(n)。
我們都已經能夠在 o(1) 時間複雜度找到要刪除的節點了,之所以還得花 o(n) 時間複雜度才能刪除,主要是時間是花在了節點前驅的查詢上,為了解決這個問題,其實,我們可以把單鏈表換成雙鏈表,這樣的話,我們就可以很好著解決這個問題了,而且,換成雙鏈表之後,你會發現,它要比單鏈表的操作簡單多了。
所以我們最後的方案是:雙鏈表 + 雜湊表,採用這兩種資料結構的組合,我們的 get 操作就可以在 o(1) 時間複雜度內完成了。由於 put 操作我們要刪除的節點一般是尾部節點,所以我們可以用乙個變數 tai 時刻記錄尾部節點的位置,這樣的話,我們的 put 操作也可以在 o(1) 時間內完成了。
具體**如下:
// 鍊錶節點的定義
class lrunode
}
// lru
public class lrucache
public void put(string key, object value)
lrunode node = map.get(key);
if (node != null) else
map.put(key, tmp);
// 插入
tmp.next = head;
head.pre = tmp;
head = tmp;}}
public object get(string key)
return null;
}private void removeandinsert(lrunode node) else if (node == tail) else
// 插入到頭結點
node.next = head;
node.pre = null;
head.pre = node;
head = node;
}}
這裡需要提醒的是,對於鍊錶這種資料結構,頭結點和尾節點是兩個比較特殊的點,如果要刪除的節點是頭結點或者尾節點,我們一般要先對他們進行處理。
這裡放一下單鏈表版本的吧
// 定義鍊錶節點
class rlunode
}// 把名字寫錯了,把 lru寫成了rlu
public class rlucache
public object get(string key)
pre = cur;
cur = cur.next;
}// 代表沒找到了節點
if (cur == null)
return null;
// 進行刪除
pre.next = cur.next;
// 刪除之後插入頭結點
cur.next = head;
head = cur;
return cur.value;
}public void put(string key, object value)
rlunode cur = head;
rlunode pre = head;
// 先檢視鍊錶是否為空
if (head == null)
// 先檢視該節點是否存在
// 第乙個節點比較特殊,先進行判斷
if (head.key.equals(key))
cur = cur.next;
while (cur != null)
pre = cur;
cur = cur.next;
}// 代表要插入的節點的 key 已存在,則進行 value 的更新
// 以及把它放到第乙個節點去
if (cur != null) else
cur.next = null;
tmp.next = head;
head = tmp;}}
}}
如果要時間,強烈建議自己手動實現一波。 經典演算法面試題 LRU快取
設計和構建乙個 最近最少使用 快取,該快取會刪除最近最少使用的專案。快取應該從鍵對映到值 允許你插入和檢索特定鍵對應的值 並在初始化時指定最大容量。當快取被填滿時,它應該刪除最近最少使用的專案。它應該支援以下操作 獲取資料 get 和 寫入資料 put 獲取資料 get key 如果金鑰 key 存...
面試題 LRU演算法及編碼實現LRU策略快取
lru least recently used 就是將最近不被訪問的資料給淘汰掉,lru基於一種假設 認為最近使用過的資料將來被使用的概率也大,最近沒有被訪問的資料將來被使用的概率比較低。lru一般通過鍊錶形式來存放快取資料,新插入或被訪問的資料放在鍊錶頭部,超過一定閾值後,自動淘汰鍊錶尾部的資料。...
鍊錶(上) 如何實現LRU快取淘汰演算法
鍊錶是一種最基礎的資料結構,學習鍊錶有什麼用?為了回答這個問題,先來討論乙個經典的鍊錶應用場景,那就是 lru 快取淘汰演算法。快取是一種提高資料讀取效能的技術,在硬體設計 軟體開發中都有著非常廣泛的應用,比如常見的 cpu 快取 資料庫快取 瀏覽器快取等等。快取的大小有限,當快取被用滿時,哪些資料...