LFU缓存O(1)均摊复杂度的关键是minFreq的高效维护:不能每次get扫描,而应在频次桶变空且等于minFreq时才递增;新key插入时置minFreq=1;淘汰前需while循环跳过空桶。

LFU 缓存要真正达到 O(1) 均摊复杂度,关键不在“怎么存数据”,而在于「minFreq 怎么找、什么时候更新、为什么不能每次 get 都扫一遍」——绝大多数超时或淘汰错位问题,都卡在这一步。
为什么 minFreq 不能每次 get() 都重新扫描
扫描所有频次桶找最小非空频次,最坏是 O(F)(F 是历史最大频次),在高频访问场景下直接退化为线性;LeetCode 460 或压测中 TLE 的主因就是这里。正确做法是只在频次桶变空且它恰好等于当前 minFreq 时才推进。
-
minFreq只会递增,不会下降:新 key 插入频次必为 1,所以put()新键时直接设minFreq = 1 -
get()提升频次后,若原频次链表变空 且old_freq == minFreq,才执行minFreq++ - 淘汰前不检查是否为空?不行——必须加
while (freqBuckets[minFreq].empty()) minFreq++,因为put()中删节点可能让minFreq桶空了但没来得及更新
freqBuckets 用 std::vector 还是 std::unordered_map?
用 std::vector<:list>></:list> 更快,前提是频次分布不极端稀疏(比如不会出现 freq=1、freq=99999 同时存在)。下标即频次,freqBuckets[freq] 是 O(1) 直接寻址,彻底避开哈希查找开销。
- 初始化时
freqBuckets.resize(2),freqBuckets[0]占位不用,freqBuckets[1]用于首次插入 - 扩容策略:每次要写入
freqBuckets[freq]但freq >= freqBuckets.size()时,freqBuckets.resize(freq + 1) - 防爆内存:设硬上限如
MAX_FREQ = 1000,超过的统一归入freqBuckets[MAX_FREQ],不再细分
节点迁移时迭代器失效怎么破
别用 std::list::iterator 当长期句柄——跨链表移动后,旧迭代器不指向任何有效位置。真实高效的做法是让每个 Node 自带指针域,管理自身在所属链表中的位置。
立即学习“C++免费学习笔记(深入)”;
-
Node必须含字段:int key, value, freq、Node* prev, *next(双向链表指针) - 每个频次桶用裸指针双向链表(非
std::list),Node的prev/next就是它在该桶内的位置,无需额外查表 -
get()时:从freqBuckets[old_freq]中摘下节点(靠prev/nextO(1) 解链),freq++,再插到freqBuckets[freq]头部(同样 O(1))
同频次淘汰选最久未用,为什么不用 std::chrono 时间戳
调用 std::chrono::steady_clock::now() 有可观开销,且纳秒精度对缓存淘汰无意义。更轻量的做法是全局单调序号。
- 声明
static std::atomic<size_t> g_tick{0}</size_t> - 每次
get()或put()访问节点时,执行node.tick = g_tick.fetch_add(1, std::memory_order_relaxed) - 同频桶内按
tick升序排:tick小的在尾,淘汰时取freqBuckets[minFreq].tail
最容易被忽略的是:minFreq 初始值设为 0 后,第一次 put() 必须显式置为 1;还有,淘汰前那个 while 循环不能省——它兜底处理了多个并发 put 导致同一频次桶被清空但 minFreq 没同步更新的情况。



















