生活随笔
收集整理的這篇文章主要介紹了
LFU缓存详解
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
LFU緩存詳解
文章目錄
一、LFU概念
LRU詳解:https://blog.csdn.net/wolfGuiDao/article/details/105862106
-
LRU 算法相當于把數據按照時間排序,這個需求借助鏈表很自然就能實現,你一直從鏈表頭部加入元素的話,越靠近頭部的元素就是新的數據,越靠近尾部的元素就是舊的數據,我們進行緩存淘汰的時候只要簡單地將尾部的元素淘汰掉就行了。
-
而 LFU 算法相當于是淘汰訪問頻次最低的數據,如果訪問頻次最低的數據有多條,需要淘汰最舊的數據。把數據按照訪問頻次進行排序,而且頻次還會不斷變化,這可不容易實現。
-
從題面上感覺并不難,不就是排個序嘛,如果這么想就錯了:因為要求O(1) 時間復雜度內執行兩項操作
二、分析
方法一:哈希表 + 平衡二叉樹
- 因為我們剛剛分析過,LFU算法關系到數據的訪問頻次和數據的新舊(即數據的訪問時間),所以我們封裝一個數據結構:
struct Node
{int cnt
;int time
;int key
, value
;bool operator< (const Node
& rhs
) const {return cnt
== rhs
.cnt
? time
< rhs
.time
: cnt
< rhs
.cnt
;}
};
- 比較直觀的想法就是我們用哈希表 key_table 以鍵 key 為索引存儲緩存,根據 (cnt,time) 雙關鍵字建立一個平衡二叉樹 S 來保持緩存
- 由于在 C++ 中,我們可以使用 STL 提供的 std::set 類,set 背后的實現是紅黑樹:
- 對于 get(key) 操作,我們只要查看一下哈希表 key_table 是否有 key 這個鍵即可,有的話需要同時更新哈希表和集合中該緩存的使用頻率以及使用時間,否則返回 -1。
- 對于 put(key, value) 操作,首先需要查看 key_table 中是否已有對應的鍵值。如果有的話操作基本等同于 get(key),不同的是需要更新緩存的 value 值。如果沒有的話相當于是新插入一個緩存,這時候需要先查看是否達到緩存容量 capacity,如果達到了的話,需要刪除最近最少使用的緩存,即平衡二叉樹中最左邊的結點,同時刪除 key_table 中對應的索引,最后向 key_table 和 S 插入新的緩存信息即可。
- 完整代碼
struct Node
{int cnt
;int time
;int key
, value
;Node(int _cnt
, int _time
, int _key
, int _value
):cnt(_cnt
), time(_time
), key(_key
), value(_value
){}bool operator < (const Node
& rhs
) const {return cnt
== rhs
.cnt
? time
< rhs
.time
: cnt
< rhs
.cnt
;}
};class LFUCache
{int capacity
;int time
;unordered_map
<int, Node
> key_table
;set
<Node
> S
;public:LFUCache(int _capacity
) {capacity
= _capacity
;time
= 0;key_table
.clear();S
.clear();}int get(int key
) {if (capacity
== 0) return -1;auto it
= key_table
.find(key
);if (it
== key_table
.end()) return -1;Node cache
= it
-> second
;S
.erase(cache
);cache
.cnt
+= 1;cache
.time
= ++time
;S
.insert(cache
);it
-> second
= cache
;return cache
.value
;}void put(int key
, int value
) {if (capacity
== 0) return;auto it
= key_table
.find(key
);if (it
== key_table
.end()) {if (key_table
.size() == capacity
) {key_table
.erase(S
.begin() -> key
);S
.erase(S
.begin());}Node cache
= Node(1, ++time
, key
, value
);key_table
.insert(make_pair(key
, cache
));S
.insert(cache
);}else {Node cache
= it
-> second
;S
.erase(cache
);cache
.cnt
+= 1;cache
.time
= ++time
;cache
.value
= value
;S
.insert(cache
);it
-> second
= cache
;}}
};
方法二:雙哈希表
-
我們定義兩個哈希表
-
第一個 freq_table 以頻率 freq 為索引,每個索引存放一個雙向鏈表,這個鏈表里存放所有使用頻率為 freq 的緩存,緩存里存放三個信息,分別為鍵 key,值 value,以及使用頻率 freq。
-
第二個 key_table 以鍵值 key 為索引,每個索引存放對應緩存在 freq_table 中鏈表里的內存地址,這樣我們就能利用兩個哈希表來使得兩個操作的時間復雜度均為 O(1)。
-
同時需要記錄一個當前緩存最少使用的頻率 minFreq,這是為了刪除操作服務的。
-
對于 get(key) 操作,我們能通過索引 key 在 key_table 中找到緩存在 freq_table 中的鏈表的內存地址,如果不存在直接返回 -1,否則我們能獲取到對應緩存的相關信息,這樣我們就能知道緩存的鍵值還有使用頻率,直接返回 key 對應的值即可。
-
但是我們注意到 get 操作后這個緩存的使用頻率加一了,所以我們需要更新緩存在哈希表 freq_table 中的位置。
-
已知這個緩存的鍵 key,值 value,以及使用頻率 freq,那么該緩存應該存放到 freq_table 中 freq + 1 索引下的鏈表中。
-
所以我們在當前鏈表中 刪除該緩存對應的節點,根據情況更新 minFreq 值,然后將其插入到 freq + 1 索引下的鏈表頭完成更新。這其中的操作復雜度均為 O(1)。
-
你可能會疑惑更新的時候為什么是插入到鏈表頭,這其實是為了保證緩存在當前鏈表中從鏈表頭到鏈表尾的插入時間是有序的,為下面的刪除操作服務。
-
對于 put(key, value) 操作,我們先通過索引 key在 key_table 中查看是否有對應的緩存,如果有的話,其實操作等價于 get(key) 操作,唯一的區別就是我們需要將當前的緩存里的值更新為 value。如果沒有的話,相當于是新加入的緩存,如果緩存已經到達容量,需要先刪除最近最少使用的緩存,再進行插入。
-
先考慮插入,由于是新插入的,所以緩存的使用頻率一定是 1,所以我們將緩存的信息插入到 freq_table 中 1 索引下的列表頭即可,同時更新 key_table[key] 的信息,以及更新 minFreq = 1。
-
那么剩下的就是刪除操作了,由于我們實時維護了 minFreq,所以我們能夠知道 freq_table 里目前最少使用頻率的索引,同時因為我們保證了鏈表中從鏈表頭到鏈表尾的插入時間是有序的,所以 freq_table[minFreq] 的鏈表中鏈表尾的節點即為使用頻率最小且插入時間最早的節點,我們刪除它同時根據情況更新 minFreq ,整個時間復雜度均為O(1)。
-
完整代碼
struct Node
{int key
, val
;int freq
;Node(int _key
,int _val
,int _freq
): key(_key
), val(_val
), freq(_freq
){}};
class LFUCache
{int minfreq
;int capacity
;unordered_map
<int, list
<Node
>::iterator
> key_table
;unordered_map
<int, list
<Node
>> freq_table
;public:LFUCache(int _capacity
) {minfreq
= 0;capacity
= _capacity
;key_table
.clear();freq_table
.clear();}int get(int key
) {if (capacity
== 0) return -1;auto it
= key_table
.find(key
);if (it
== key_table
.end()) return -1;list
<Node
>::iterator node
= it
-> second
;int val
= node
-> val
, freq
= node
-> freq
;freq_table
[freq
].erase(node
);if (freq_table
[freq
].size() == 0) {freq_table
.erase(freq
);if (minfreq
== freq
) minfreq
+= 1;}freq_table
[freq
+ 1].push_front(Node(key
, val
, freq
+ 1));key_table
[key
] = freq_table
[freq
+ 1].begin();return val
;}void put(int key
, int value
) {if (capacity
== 0) return;auto it
= key_table
.find(key
);if (it
== key_table
.end()) {if (key_table
.size() == capacity
) {auto it2
= freq_table
[minfreq
].back();key_table
.erase(it2
.key
);freq_table
[minfreq
].pop_back();if (freq_table
[minfreq
].size() == 0) {freq_table
.erase(minfreq
);}}freq_table
[1].push_front(Node(key
, value
, 1));key_table
[key
] = freq_table
[1].begin();minfreq
= 1;} else {list
<Node
>::iterator node
= it
-> second
;int freq
= node
-> freq
;freq_table
[freq
].erase(node
);if (freq_table
[freq
].size() == 0) {freq_table
.erase(freq
);if (minfreq
== freq
) minfreq
+= 1;}freq_table
[freq
+ 1].push_front(Node(key
, value
, freq
+ 1));key_table
[key
] = freq_table
[freq
+ 1].begin();}}
};
總結
以上是生活随笔為你收集整理的LFU缓存详解的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。