LFU 缓存算法详解:双哈希 + 双向链表实现 O(1) 操作
LFU 缓存算法依据历史访问频率淘汰数据,优先保留长期热点。核心实现采用双哈希表配合频率双向链表,确保查询、更新、淘汰均为 O(1) 时间复杂度。主要优势是稳定保护高频数据,劣势包括新数据冷启动困难及旧高频数据霸占缓存。可通过频率衰减、初始频率加成或与 LRU 混合策略进行优化。适用于 CDN 热门视频、电商爆款商品等访问模式稳定的场景。

LFU 缓存算法依据历史访问频率淘汰数据,优先保留长期热点。核心实现采用双哈希表配合频率双向链表,确保查询、更新、淘汰均为 O(1) 时间复杂度。主要优势是稳定保护高频数据,劣势包括新数据冷启动困难及旧高频数据霸占缓存。可通过频率衰减、初始频率加成或与 LRU 混合策略进行优化。适用于 CDN 热门视频、电商爆款商品等访问模式稳定的场景。

基础规则: 优先淘汰历史访问频率最低的数据(长期统计维度)。
操作流程:
LFU 通常通过双哈希表 + 频率双向链表的组合实现 O(1) 时间复杂度的操作:
例如:当数据 A 被访问时,其频率从 1→2,需从原频率 1 的链表移除,并插入到频率 2 的链表头部;若新数据 B 写入,初始频率为 1,插入频率 1 链表。
每次访问(读/写)缓存数据时,触发频率 +1 的更新操作:
我们下面要设计的这个 LFU 算法逻辑和上面的演示图是差不多,只不过我们存储的不是自定义节点的地址,而是这个 Node 对象,当然地址也行,其他都差不多。
对应的节点结构如下:
// 定义对应的节点结构:template<class K, class V>
struct Node {
Node() {}
// 表明对应的默认对象必须存在,hash[key] = Node()!!
Node(const K& key, const V& value, int access_count)
: key(key), value(value), access_count(access_count) {}
K key;
V value; // 存储对应 key 的访问次数
int access_count;
// 对应节点的 key 值所在的 list 处的迭代器
typename std::list<K>::iterator it;
};
然后我们只要的就是维护两个哈希,以及容量和对应最小频率计数器即可:
std::unordered_map<K, Node<K, V>> _hash_map; // 储存对应 key 值与对应的上述节点的 hash
std::unordered_map<int, std::list<K>> _access_map; // 储存对应的访问次数的存放 key 的那个 list
size_t _capacity; // 缓存最大容量,根据它进行决定进行对应的 LFU 策略更新
int _min_access_count; // 储存最少的访问次数,方便进行删除
然而 LFU,最大的特色就在于它的访问频率更新策略,通过它来保证整个算法的优越性,下面我们解释下这个策略:
需要注意的是:
// 对于 put 或者 get 后进行对应的数据的访问次数等操作进行统一处理(比如进行 LFU 策略删除等)
void accessCountAdd(const K& key) {
// 获取对应节点
Node<K, V>& node = _hash_map[key];
int old_access_cnt = node.access_count;
std::list<int>& cur_list = _access_map[old_access_cnt];
cur_list.erase(node.it); // 删除后进行判断,如果访问的 hash 对应 list 空了,直接删除这个 hash 槽即可
if (cur_list.empty()) {
_access_map.erase(old_access_cnt);
// 如果恰好是最少的,更新下最小的即可
if (old_access_cnt == _min_access_count)
// 如果删除完对应的之前的 key 所在的 list 后,为空了而且此时对应的 min_cnt 也是当前 key 的 cnt,此时说明 min_cnt 必须要 +1 完成更新了
_min_access_count++;
}
// 此时除了有可能它俩相同,就是 cur_list 不为空的情况,或者不等:
// 进行更新访问次数:
int new_access_cnt = old_access_cnt + 1;
node.access_count = new_access_cnt;
// 再次更新下,这里保险起见:
_min_access_count = std::min(_min_access_count, new_access_cnt);
// 把对应的 key 加入到新的 list 中:
auto& new_list = _access_map[new_access_cnt];
// 这里需要引用,因为后续会进行修改
new_list.push_front(node.key);
// 务必要更新对应迭代器:
node.it = new_list.begin();
}
然后我们主要提供对应的 get 与 put 接口:
1. get: 这里其实就是通过快查 hash,完成对应的获取,如果不存在返回默认构造(这里可能有点不合适,因为是模版暂时这样),如果存在就返回对应 node 的 value,然后进行频率更新策略。
V get(const K& key) {
// 先判断是否存在这个 key:
if (_hash_map.find(key) == _hash_map.end())
return V(); // 有点瑕疵,一般都是 int int,直接返回 -1
// 存在这个 key,进行访问次数 +1:
accessCountAdd(key);
return _hash_map[key].value;
}
2. put: 首先先去第一个 hash 找对应 key 的节点是否存在,分为两种情况:
bool put(const K& key, const V& val) {
// 1. 判断容量是否为 0:
if (_capacity <= 0) return false;
// 进行查找:
auto it = _hash_map.find(key);
if (it != _hash_map.end()) {
// 存在这个 key,直接更新对应的值并根据对应 LFU 策略进行对应处理:
_hash_map[key].value = val;
// 2. 更新访问次数:
accessCountAdd(key);
return true;
}
// 不存在,检查容量(根据 LFU 策略更新),插入新的:
// it==_hash_map.end()
if (_capacity <= _hash_map.size()) {
// 容量已满,执行 LFU 策略:
// 找到访问次数最少的 list,也就是是最不经常使用的那一批数据:
auto& min_list = _access_map[_min_access_count];
// 根据 LRU 策略删除最近最少使用的 key:
auto evict_key = min_list.back();
min_list.pop_back();
// 判断是否这个 list 为空了,进行删除:
if (min_list.empty()) _access_map.erase(_min_access_count);
_hash_map.erase(evict_key);
}
// 不存在直接构造插入:
// 3. 插入新节点:
auto& new_list = _access_map[1]; // 这里是 1,因为是新插入的节点,访问次数为 1
_hash_map[key] = Node<K, V>(key, val, 1);
// 这里也需要告诉编译器是个类型不是变量!!
// 4. 更新访问次数 hash:
new_list.(key);
_hash_map[key].it = new_list.();
_min_access_count = ;
;
}
注: 这里有个坑点:就是必须先从对应 hash 中查找对应的 node,如果不存在的话,再去判断是否满了,进行删除以及后面的插入操作,不能上来就进行判满逻辑;否则比如满了,删了一个数据 1–2,但是插入的数据 key 存在,直接更新就好,因此这个 1–2 就相当于被误删了!
首先我们要知道: LFU 的核心需求是:缓存满时快速淘汰「访问频率最低」的数据,且每次访问数据时要快速更新其频率。如果用普通数组或单链表,找最低频率或移动节点都得遍历,效率太低(O(n))。
因此需要:
设计策略:
效果:
| 场景类型 | 具体例子 | 为什么适合 LFU? |
|---|---|---|
| 长期稳定热点 | CDN 热门视频缓存、经典文档/教程页 | 这些内容长期被高频访问,频率持续累积,LFU 能确保其长期驻留缓存。 |
| 爆款商品/内容 | 电商平台的限时爆款、社交媒体的热帖 | 爆款期间访问频率极高,LFU 会优先保留,支撑高并发请求。 |
| 低更新频率服务 | 在线词典的热门词条、百科经典词条 | 词条热度长期稳定,高频词条(如'人工智能')会被持续缓存。 |
| 维度 | LFU(最不经常使用) | LRU(最近最少使用) |
|---|---|---|
| 判断依据 | 历史访问频率(总次数) | 最近一次访问时间(时间戳) |
| 核心优势 | 保护长期高频热点数据,命中率高 | 快速响应新流量,适应突发热点 |
| 主要劣势 | 新数据易被淘汰,旧高频数据可能霸占 | 旧高频数据可能被误淘汰,不保护长期热点 |
| 实现复杂度 | 较高(需维护频率结构) | 较低(只需维护访问时间顺序) |
| 典型场景 | 经典内容、爆款商品、CDN 稳定热点 | 浏览器缓存、数据库 Buffer Pool、新热点 |
| 变种优化 | LFU-LRU 混合、频率衰减、初始频率加成 | LRU-K(考虑最近 K 次访问)、ARC(自适应) |
LFU 是缓存中的'时间的朋友'——谁被访问的次数越多,谁就越容易被保留;但它需要解决'新数据入场难'和'旧热点占位'的问题,实际常通过优化策略(如频率衰减、初始加成)或与 LRU 结合(如 Redis 的近似 LFU)来平衡长期与短期热点需求。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online