C++实现支持泛型的LFU详解

发布时间:2026/11/2 11:26:04

C++实现支持泛型的LFU详解 前言LFUleast frequently used最不经常使用是一种缓存淘汰策略缓存满时优先淘汰访问次数最少的元素次数相同时淘汰最久未使用的那个。它和 LRUleast recently used的区别在于LRU 只看最近一次访问时间LFU 还看累计访问频率。常见误解是LFU 用一个计数器排序就行。每次访问都对所有元素排序复杂度是 O(n log n)缓存大了根本不可行。正确的实现要维护频率 → 该频率下的元素链表这样的桶结构让 get 和 put 都达到平均 O(1)。另一个难点是最少频率的维护。删除元素后最小频率可能变化新插入的元素频率为 1最小频率又要重置。这些边界处理不好就会出现淘汰了不该淘汰的元素、或者访问已删除元素的问题。本文用 C17 模板实现一个泛型的 LFU 缓存键和值都可以是任意可哈希、可拷贝的类型并用std::list保证迭代器稳定。一、数据结构设计核心是三层结构结构类型作用索引unordered_mapKey, Entry按 key 找到元素位置与频率频率桶unordered_mapsize_t, listpairKey, Value每个频率一条链表最小频率size_t m_minFreq淘汰时直接定位到最低频桶每个Entry保存两个东西当前频率以及该元素在对应频率链表中的迭代器。std::list的插入、删除不会让其他元素的迭代器失效这让从旧频率桶搬到新频率桶成为 O(1) 操作。同一频率内链表头部放最近访问的元素尾部放最久未访问的。淘汰时取尾部就同时满足了频率最低和同频率最久未用。// 适用于 C17#include cstddef#include functional#include list#include stdexcept#include unordered_map#include utilitytemplate typename Key, typename Value,typename Hash std::hashKey,typename Equal std::equal_toKeyclass LFUCache {public:explicit LFUCache(std::size_t capacity): m_capacity(capacity){}bool get(const Key key, Value out){const auto it m_index.find(key);if (it m_index.end()) {return false;}out it-second.pos-second; // 先拷贝值再搬桶touch(it-second, it-first);return true;}void put(const Key key, const Value value){if (m_capacity 0) {return;}const auto it m_index.find(key);if (it ! m_index.end()) {it-second.pos-second value;touch(it-second, it-first);return;}if (m_index.size() m_capacity) {evict();}Bucket bucket m_freq[1];bucket.push_front(std::make_pair(key, value));m_index.emplace(key, Entry{1, bucket.begin()});m_minFreq 1;}std::size_t size() const { return m_index.size(); }private:using Bucket std::liststd::pairKey, Value;struct Entry {std::size_t freq;typename Bucket::iterator pos;};void touch(Entry entry, const Key key){const std::size_t oldFreq entry.freq;const Value value entry.pos-second; // 搬桶前保存值auto oldIt m_freq.find(oldFreq);oldIt-second.erase(entry.pos);if (oldIt-second.empty()) {m_freq.erase(oldIt);if (m_minFreq oldFreq) {m_minFreq; // 最低频桶空了整体上移}}const std::size_t newFreq oldFreq 1;Bucket bucket m_freq[newFreq];bucket.push_front(std::make_pair(key, value));entry.freq newFreq;entry.pos bucket.begin();}void evict(){auto it m_freq.find(m_minFreq);if (it m_freq.end() || it-second.empty()) {return;}Bucket bucket it-second;const Key victimKey bucket.back().first;m_index.erase(victimKey);bucket.pop_back();if (bucket.empty()) {m_freq.erase(it);}}std::size_t m_capacity;std::size_t m_minFreq 0;std::unordered_mapKey, Entry, Hash, Equal m_index;std::unordered_mapstd::size_t, Bucket m_freq;};二、get 与 put 的流程get分三步查找索引把值拷贝到输出参数把该元素从旧频率桶搬到freq 1的桶头。值必须在搬桶前拷贝因为搬桶会先删除旧的链表结点。put分两种情况key 已存在更新值然后同样提升频率key 不存在缓存满则先淘汰再以频率 1 插入新桶并把m_minFreq重置为 1。m_minFreq的重置很关键。即使之前的最小频率是 5新元素的频率是 1之后淘汰必须从 1 开始找。三、touch 的边界touch里的if (m_minFreq oldFreq) m_minFreq;只在该频率桶变空时执行。为什么可以直接加一因为刚被访问的元素从oldFreq搬到了oldFreq 1如果oldFreq是最小频率且桶已空那么新的最小频率至少是oldFreq 1而这个元素正好在oldFreq 1桶里所以最小值就是它。如果oldFreq不是最小频率桶空了也不影响m_minFreq因为还有更低的频率存在。evict取bucket.back()即同频率里最久未访问的元素。删除索引项、弹出链表尾如果桶空了就把整个频率项从 map 里移除。四、使用示例// 适用于 C17#include iostream#include stringint main(){LFUCacheint, std::string cache(2);cache.put(1, one);cache.put(2, two);std::string out;if (cache.get(1, out)) {std::cout get 1: out \n; // one}cache.put(3, three); // 淘汰 2std::cout std::boolalpha;std::cout cache.get(2, out) \n; // falsestd::cout cache.get(1, out) out \n; // true onestd::cout cache.get(3, out) out \n; // true threereturn 0;}按语义推演插入 1、2 后两者频率都是 1访问 1 后它的频率变为 2最小频率仍是 1桶里只剩 2插入 3 时缓存已满淘汰频率 1 的 2。之后查询 2 失败查询 1 和 3 都能命中。五、泛型约束与复杂度模板参数Key需要满足可用std::hashKey计算哈希或通过Hash参数传入自定义哈希可用std::equal_toKey比较相等或通过Equal传入。Value需要可拷贝因为get要把值写到输出参数touch搬桶时也要复制一次。如果值很大可以考虑让get返回指针或std::reference_wrapper但要小心元素被淘汰后指针悬垂。操作平均复杂度说明getO(1)哈希查找 链表搬移put已存在O(1)更新 搬移put新键O(1)可能触发一次淘汰空间O(capacity)索引与频率桶各一份哈希表的 O(1) 是平均情况最坏情况取决于哈希质量和冲突处理标准不作常数保证。六、与 LRU 的比较维度LRULFU淘汰依据最久未访问访问次数最少额外结构一条双向链表频率桶 最小频率对新元素立即变为最新从频率 1 开始可能很快被淘汰适用场景访问模式随时间变化热点长期稳定LFU 的问题是历史包袱一个曾经很热、后来不再使用的元素频率很高很难被淘汰。实际系统里常用带衰减的 LFU或者 LRU 与 LFU 的混合策略。常见坑点❌ 每次访问都对所有元素按频率排序。✅ 用频率桶get/put 平均 O(1)。❌ 搬桶时先删除旧结点再读它的值。✅ 先把值拷贝出来再erase。❌ 桶空了不更新m_minFreq淘汰时找不到桶。✅ 旧桶为空且等于最小频率时m_minFreq。❌ 插入新元素后忘记把m_minFreq重置为 1。✅ 新元素频率总是 1最小频率必然是 1。❌ 用std::vector存同频率元素删除导致迭代器失效。✅ 用std::list删除只影响被删结点。❌capacity为 0 时仍执行插入。✅ 开头判断直接返回。❌ 淘汰时只从频率桶删除忘记同步删除索引。✅ 两处都要删否则索引指向已释放结点。❌ 认为哈希表查找一定是常数时间。✅ 那是平均复杂度冲突严重时会退化。总结组件作用关键操作m_indexkey → 频率与链表位置查找、删除m_freq频率 → 元素链表搬入、搬出、取尾部m_minFreq定位淘汰桶桶空时上移新插入时重置Entry::pos指向链表结点使搬桶为 O(1)LFU 的代码量比 LRU 大但结构清晰索引负责找到频率桶负责排序最小频率负责淘汰。把搬桶时的值拷贝、空桶时的最小频率更新、淘汰时的两处删除这三件事做对O(1) 的 LFU 就成立了。
延伸阅读

更多相关文章

2026/11/2 11:26:04

土豆病害检测数据集实战:8908张图、5类标注与YOLO训练避坑指南

简介:这份资源是面向农业视觉检测研究者与算法开发者的土豆病害检测数据集说明文档,聚焦于利用图像识别技术对土豆在生长、储存环节中的常见病害进行早期识别与干预。数据集共含8908张标注图片,覆盖损伤、缺陷、真菌性病害、正常及萌芽五类土…

2026/11/2 11:21:02

一物一码红包营销落地实战指南

在快消品行业,促销活动往往是拉动销量的关键引擎,但传统的“普惠式”打折或固定金额红包,不仅让营销成本居高不下,还难以激发用户的复购热情。更棘手的是,一旦活动火爆,瞬间涌入的高并发流量极易导致系统崩…

2026/11/2 11:21:02

02 词法与语法分析(前端)

02 词法与语法分析(前端) 本册目标:搞清楚源代码文本如何变成语法树——Lexer 如何扫描 Token、LanguageParser 如何用递归下降 优先级爬升建树、红绿树为何能做到编辑时 O(1) 复用、Syntax API 长什么样、语法错误如何恢复。 一、要点速览 …

2026/11/2 12:31:46

企业级WAF与金蝶天燕AAS集成安全防护实战

1. 企业级应用安全防线的核心挑战 在数字化转型浪潮下,企业级应用面临的安全威胁呈现指数级增长。去年某金融机构因Web应用漏洞导致的数据泄露事件,直接损失超过2.3亿元。这让我深刻意识到,传统的防火墙和杀毒软件已经无法应对现代应用层的复…

2026/11/2 12:31:46

AI时代数据可视化:工具链革新与实战应用

1. 项目概述:AI时代的数据可视化新范式 数据可视化早已不再是简单的图表绘制,而是数据思维与设计美学的融合表达。过去半年,我使用各类AI工具完成了47个商业数据分析项目,发现传统工具链正在被颠覆——现在用AI生成专业图表的时间…

2026/11/2 12:31:46

数字电路实验进阶:模7计数器、N=5分频与序列发生器设计

简介:这份资源是电工电子基础实验B中计数与分频电路的完整实验报告,面向具备基础电路知识的高校计算机及电子相关专业学生与初学者,帮助其掌握计数器设计、整数分频与序列信号生成等数字电路核心技能。报告围绕CB4CLE计数器展开,分…

2026/11/2 12:31:46

Metashape 1.82 Python API 参考手册:自动化航测与建模脚本指南

简介:这份PDF面向使用 Metashape Professional 进行摄影测量与三维重建的开发者,系统讲解其 Python API 1.82 的调用方式与核心模块,适合已具备 Python 基础、希望将建模流程自动化的中高级用户。文档围绕 Metashape 模块展开,涵盖…

2026/11/2 12:31:46

Text2SQL选型:4步法中的场景定义与标准设定

1. 为什么选型这么难:Text2SQL 项目的第一道分水岭 Text2SQL 这几年热度一直没降,但真正落地的团队其实没有想象中那么多。我见过不少团队在选型阶段反复横跳,今天拿开源模型微调,明天换 API 试用,折腾两三个月后还在原…

2026/11/2 12:26:45

GIS与遥感技术在土地覆盖分类中的应用与实践

1. 项目背景与核心价值1996-2001年间新罕布什尔州的土地覆盖与土地利用分类项目,是地理信息系统(GIS)和遥感技术应用于区域环境监测的典型案例。这个看似简单的标题背后,实际上包含了一套完整的地表观测技术体系——通过卫星遥感影…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑