【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:
- 缓存满时,删除最久未使用的元素;假设有两个元素 a,b,前 3 次使用 a,最后一个使用 b,则虽然 a 被使用了 3 次,但由于我们最后一次使用的是 b,所以需要删除 a。
LFU:
- 缓存满时,删除使用频次最少的元素;假设有两个元素 a,b,前 3 次使用 a,最后一个使用 b,由于 b 使用的频次少于 a,所以删除 b。
所以,在 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/