【LeetCode-模擬】LFU快取
題目描述
請你為 最不經常使用(LFU)快取演算法設計並實現資料結構。它應該支援以下操作:get 和 put。
- get(key) - 如果鍵存在於快取中,則獲取鍵的值(總是正數),否則返回 -1。
- put(key, value) - 如果鍵已存在,則變更其值;如果鍵不存在,請插入鍵值對。當快取達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除最久未使用的鍵。
「項的使用次數」就是自插入該項以來對其呼叫 get 和 put 函式的次數之和。使用次數會在對應項被移除後置為 0 。
進階:
你是否可以在 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
題目連結: https://leetcode-cn.com/problems/lfu-cache/
思路
這題和LRU快取機制很像。
LRU:
- 快取滿時,刪除最久未使用的元素;
LFU:
- 快取滿時,刪除使用頻次最少的元素;
所以,在 LFU 中,我們還需要記錄每個元素被訪問的次數,每個元素除了 key,val 之外,還需要定義 freq 表示訪問的次數:
struct Node{ int key; int val; int freq; Node(int key, int val, int freq):key(key), val(val), freq(freq){} };
我們使用兩個雜湊表:unordered_map<int, list<Node>::iterator> keyTable
用來儲存 key 到連結串列節點指標的對映;unordered_map<int, list<Node>> freqTable
用來儲存頻數 freq 到連結串列的對映,具體如下圖:
圖來自這篇題解,左邊的雜湊表為 keyTable,右邊的雜湊表為 freqTale。可以看到 freqTable 根據頻數儲存了具體的連結串列,而 keyTable 儲存了 key 到連結串列節點地址的對映。
除此之外,我們還需要一個變數 minFreq
來儲存目前最少訪問的次數,通過 freqTable[minFreq].pop_back() 刪除使用最少的節點。
演算法步驟:
-
get(key):
- 如果 key 不在 keyTable 中,返回 -1;
- 否則,通過 keyTable[key] 獲取 key 對應的節點地址 it,然後通過 it 得到節點的 key、val、freq,將節點從 freqTable[freq] 對應的連結串列刪除,然後將該節點加入到 freqTable[freq+1] 對應的連結串列頭;
-
put(key, value):
- 如果 key 在 keyTable 中,則使用和 get(key) 中的第二步類似的方法;
- 否則,如果快取已經滿了,則根據 minFreq 刪除使用最少的節點,然後設定 minFreq 為 1,將新的節點放入 freqTable[1] 對應的連結串列頭。
具體程式碼如下:
struct Node{
int key;
int val;
int freq;
Node(int key, int val, int freq):key(key), val(val), freq(freq){}
};
class LFUCache {
private:
unordered_map<int, list<Node>::iterator> keyTable;
unordered_map<int, list<Node>> freqTable;
int capacity;
int minFreq;
public:
LFUCache(int capacity) {
this->capacity = capacity;
this->minFreq = 0;
}
int get(int key) {
if(keyTable.count(key)==0) return -1;
else{
auto it = keyTable[key]; // it 為 key 對應的節點地址
int val = it->val;
int freq = it->freq;
freqTable[freq].erase(it); // 在 freqTable[freq] 對應的連結串列中刪除節點
if(freqTable[freq].size()==0){ // 如果刪除後 freqTable[freq] 為空
freqTable.erase(freq);
if(minFreq==freq) minFreq++; // 注意這一步
}
freqTable[freq+1].push_front(Node(key, val, freq+1)); // 將節點放入 freqTable[freq+1] 對應的連結串列中
keyTable[key] = freqTable[freq+1].begin();
return val;
}
}
void put(int key, int value) {
if(this->capacity==0) return; // 注意判斷容量是否為 0
if(keyTable.count(key)!=0){ // key 已經在快取中了
auto it = keyTable[key]; // 下面的步驟和 get 函式中 else 部分基本相同
int freq = it->freq;
freqTable[freq].erase(it);
if(freqTable[freq].size()==0){
freqTable.erase(freq);
if(minFreq==freq) minFreq++;
}
freqTable[freq+1].push_front(Node(key, value, freq+1));
keyTable[key] = freqTable[freq+1].begin();
}
else{ // key 不在快取中
if(keyTable.size()==this->capacity){ // 快取容量已滿
Node node = freqTable[minFreq].back(); // 通過 minFreq 找到使用最少的節點 back()
keyTable.erase(node.key); // 刪除使用最少的節點
freqTable[minFreq].pop_back();
if(freqTable[minFreq].empty()){
freqTable.erase(minFreq);
}
}
freqTable[1].push_front(Node(key, value, 1));
keyTable[key] = freqTable[1].begin();
minFreq = 1; // minFreq 置為 1
}
}
};
/**
* Your LFUCache object will be instantiated and called as such:
* LFUCache* obj = new LFUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
注意點:
- 在 freqTable[freq] 對應的連結串列中插入節點時,都是插入連結串列頭 (push_front());
- 每次在 freqTable[freq] 對應的連結串列中刪除節點後,都要判斷 freqTable[freq] 是否為空,如果為空,則將 freq 從 freqTable 中刪除,並在必要的情況下更新 minFreq。
參考
1、https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-shuang-xiang-lian-biao-lfuhuan-cun-by-realzz/
2、https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-biao-shuang-xiang-lian-biao-java-by-liweiwei/