LRU算法和LFU算法是屬於頁面置換的一種算法,或者更通俗的說,就是緩存如何淘汰的一種策略。
我們通常在設計一個系統的時候,由於資料庫的讀取速度遠小於內存的讀取速度,所以為了加快讀取速度,會將一部分數據放到內存中,稱為緩存。
但是內存容量是有限的,當你要緩存的數據超出容量,就得有部分數據刪除,這時候哪些數據刪除,哪些數據保留,就是LRU算法和LFU算法要幹的事。
LRU算法,全稱Least recently used,即最近最少使用。LRU算法的思想是如果數據最近被訪問過,那麼將來被訪問的概率也會很高。
根據這個思想,我們可以想到,實現LRU算法肯定會用到列表/鍊表,目的是保證有序;還有一個是用到哈希表,這是因為緩存經常是key-value鍵值對的形式。
比較簡單的做法是使用列表+哈希表,但是這種方式查詢和插入的時間複雜度都是O(n),還有一種做法是使用雙向鍊表+哈希表,查詢和插入的時間複雜度都是O(1),但是耗費的空間資源比較多。
如上圖,假設我們使用頭插法,即最近訪問的元素放在最前面,最晚的元素放在最後面,則實現LRU算法的步驟如下:
這應該是面試比較常考的點,面試官可能會問你,如果實現一個時間複雜度為O(1)的LRU緩存?
這種實現的結構如下:
上述LRUCache的m其實就是上圖左邊的HashMap,它的目的是為了實現查找的時間複雜度為O(1)。
如果沒有這個m,則查找元素的時候,需要遍歷雙向鍊表,時間複雜度為O(n)。
而使用雙向鍊表的原因主要是為了實現節點插入/刪除的時間複雜度為O(1)。
那使用單鍊表不行嗎?
如上,使用單鍊表的話,可以實現頭部快速插入新節點,尾部快速刪除舊節點,時間複雜度都是O(1)。
但是對於中間節點,比如我需要節點1的值由2更新為4,這時候除了更新值,還需要將其移動到最前面,而對於單鍊表,它只知道下一個元素,並不知道上一個元素,為了得到上一個元素,它必須遍歷一次鍊表才知道,時間複雜度為O(n),這就是為什麼要用雙向鍊表的原因。
關於這種方式的源碼實現,可以查看Leetcode LRU雙向鍊表實現
LFU算法,全稱Least frequently used,即最不經常使用。LFU算法的思想是一定時期內被訪問次數最少的節點,在將來被訪問到的機率也是最小的。
由此可以看到,LFU強調的是訪問次數,而LRU強調的是訪問時間。
LFU有兩種實現方式,一是哈希表+平衡二叉樹,二是雙哈希表,下面以雙哈希表為例,說明LFU具體的步驟:
雙哈希表的實現如下圖:
如上,雙哈希表需要維護兩個哈希表以及一個最少頻次使用的計數minFreq。
第一個哈希表是 freq_table,它的key是訪問的頻次,它的value是一個雙向鍊表,雙向鍊表的每一個節點包含三個元素:key,value,以及count。
第二個哈希表是 key_table,它的key是雙向鍊表中存儲的key,value是對應節點的指針(這樣查找的時間複雜度就是O(1))。
類比LRU,LFU的步驟如下: