上一篇博客我们实现了LRU缓存算法。
进一步地,我们思考一下LFU算法又该如何实现呢?与LRU算法相比,LFU算法要求当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用 的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。也就是说LFU算法实现比LRU算法要考虑的更多一步,也就是要考虑每个缓存内容的使用频率。
思路
在上一篇LRU算法实现过程中,我们提到了可以将缓存的更新和替换看成对一堆书进行操作,每次get和put已有的书时,则将书放到书堆的最上面,当有新的书加入时,则将最下面的书取出,并将新的书放到最上面。
对于LFU算法的实现,我们同样可以将其看成对书堆的操作,不同的是,我们需要看成对多个书堆的操作,每本书用一个使用频率freq
来进行维护其使用次数。当对一本书进行get和put操作时,其频率加一,同时每个书堆分别代表不同使用频率的书的集合,每个集合则用LRU算法进行维护书堆。
那么。
- 如何实现好几堆书呢?
同样,可以用哈希表实现,哈希表的键表示频率,哈希表的值则是双链表的头结点。这样我们就能在O(1)的时间复杂度获取相应频率的书堆。
- 超过缓存容量时,如何快速获取到使用频率最小的书呢?
我们可以维护一个min_freq的变量记录使用频率最小的数。在添加一本新书的情况下,这本新书一定是放在 f r e q = 1 freq=1 freq=1 的那摞书上,此时我们把 min_freq置为 1。在「抽出一本书且这摞书变成空」的情况下,我们会把这本书放到它右边这摞书的最上面。如果变成空的那摞书是最左边的,我们还会把 min_freq加一。所以无论如何,min_freq都会对应着最左边的非空的那摞书。相应的,如果有新的书放到这几个书堆中来,则这本新书的使用频率肯定是最小的并且为1,此时可以更新min_freq的值为1。
算法分析
-
时间复杂度:所有操作均为O(1)
-
空间复杂度:O(capacity)
-
淘汰策略:
优先淘汰最低频率节点
同频率时淘汰最久未使用
优化亮点
-
双哈希表结构:实现快速访问和频率管理
-
哨兵节点:简化链表边界操作
-
动态频率维护:自动跟踪最小频率
-
LRU辅助策略:在相同频率下保留访问时序
代码实现
cpp
class Node {
public:
int key;
int value;
int freq;
Node *next;
Node *prev;
Node(int k = 0, int v = 0, int f = 1) : key(k), value(v), freq(f){}
};
class LFUCache {
private:
int capacity;
int min_freq;
unordered_map <int, Node*> key_to_node;
unordered_map <int, Node*> freq_to_node;
void RemoveNode(int key) {
Node *node = key_to_node[key];
node -> prev -> next = node -> next;
node -> next -> prev = node -> prev;
int freq = node -> freq;
key_to_node.erase(key);
if(freq_to_node[min_freq] -> next == freq_to_node[min_freq]) {
delete freq_to_node[min_freq];
freq_to_node.erase(min_freq);
// 如果被删除的是当前最小频率链表,则最小频率+1
if(min_freq == freq) min_freq++;
}
delete node;
}
void PushFront(Node *node) { // 头插法
Node *cache;
int freq = node -> freq;
// 如果该频率没有链表,则新建哨兵节点
if(freq_to_node.find(freq) == freq_to_node.end()) {
cache = new Node(0, 0); // 创建一个哨兵结点
cache -> next = cache -> prev = cache;
freq_to_node[freq] = cache;
} else {
cache = freq_to_node[freq];
}
node -> next = cache -> next;
node -> prev = cache;
cache -> next -> prev = node;
cache -> next = node;
key_to_node[node -> key] = node;
}
public:
LFUCache(int capacity) {
this -> capacity = capacity;
min_freq = 1; // 初始最小频率为1(新节点频率都是1)
}
int get(int key) {
if(key_to_node.find(key) != key_to_node.end()) {
int value = key_to_node[key] -> value;
int freq = key_to_node[key] -> freq + 1;
RemoveNode(key); // 从旧频率链表移除
Node *node = new Node(key, value, freq);
PushFront(node); // 插入新频率链表
return value;
}
return -1;
}
void put(int key, int value) {
auto find_key = key_to_node.find(key);
if(find_key == key_to_node.end()) {
Node *node = new Node(key, value);
if(key_to_node.size() < capacity) {
PushFront(node);
} else {
RemoveNode(freq_to_node[min_freq] -> prev -> key);
PushFront(node);
}
min_freq = 1; // 如果是新内容
} else { // 如果内容已经存在
int freq = key_to_node[key] -> freq;
RemoveNode(key);
Node *node = new Node(key, value, freq + 1);
PushFront(node);
}
}
};
/**
* 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);
*/