用std::list+std::unordered_map实现lru最稳:map存list迭代器以支持o(1)splice,get/put均常数时间;淘汰时须同步删list和map,构造时校验容量合法性,严禁复用可能失效的迭代器。

用 std::list + std::unordered_map 是最稳的组合
直接上手写 LRU,别碰手撸双向链表或自己管理内存——C++ 标准库的 std::list 保证节点删除 O(1),std::unordered_map 查 key 也是平均 O(1)。两者配合,get 和 put 都能稳在常数时间。
常见错误是把 std::list 当成“只能从头尾操作”的队列用,结果每次 get 都得遍历找节点再 splice 到 front,退化成 O(n)。正确做法是 map 存的是 std::list<pair int>>::iterator</pair>,这样拿到迭代器就能直接 splice。
- map 的 value 类型必须是迭代器(不是指针!),否则无法安全移动节点
-
list.splice(list.begin(), list, it)是把it指向的节点移到开头,不是复制 - 插入新元素前,先检查是否已存在:存在就更新值 + 移动到 front;不存在才新建节点并可能触发淘汰
erase 时别只删 map 不清 list
淘汰最久未用项(即 list.back())时,必须同步从 unordered_map 中擦除对应 key,否则 map 里留着悬空迭代器,下次访问直接 UB(通常表现为崩溃或随机值)。
典型翻车现场:map.erase(list.back().first); list.pop_back(); 看似合理,但如果 map 里没这个 key(比如中途被删过但 list 没同步),erase 无事发生,而 pop_back 还是执行了——list 少一节点,map 多一个无效迭代器,后续任意 get 都可能解引用野指针。
立即学习“C++免费学习笔记(深入)”;
- 务必先用
auto it = map.find(key)检查存在性,再同时操作 list 和 map - 淘汰逻辑建议封装成独立函数,避免漏掉任一边
- 调试时可加断言:
assert(map.count(key) == 0 || map[key] != list.end())
构造函数里别传负容量或零容量
LRU 缓存容量为 0 或负数时,put 应该拒绝任何插入,get 全部返回 -1。但很多人直接用 capacity 做 guard,却忘了在构造时没校验——导致后续所有操作逻辑混乱,比如 <code>list.size() > capacity 永远不成立,淘汰逻辑压根不触发。
更隐蔽的问题是:某些实现用 capacity = max(1, cap) 强行兜底,看似安全,实则掩盖了调用方传入非法值的事实,后期扩容策略变更时容易出错。
- 构造函数第一行就做
if (capacity - 或者接受 0 容量,但内部保持 list 为空、map 为空,并让所有操作快速返回默认值
- 别依赖运行时“看起来没崩”来验证逻辑,单元测试必须覆盖
LRUCache(0)和LRUCache(-1)
注意 std::list::splice 的迭代器失效规则
splice 不会使被移动节点的迭代器失效,但**源容器的 end() 迭代器会变**——不过你一般不会拿它做别的事。真正容易踩的是:对同一个 list 多次 splice 后,原迭代器仍有效,但若中间混入 push_front、erase 等操作,其他无关迭代器也可能失效。
例如:缓存满时先 pop_back,再 push_front 新节点,最后用 map 里存的老迭代器去 splice——这迭代器早因 pop_back 或 push_front 失效了(虽然 std::list 的 push/pop 不影响其他节点迭代器,但初学者常误以为所有操作都安全)。
- 只对 map 中保存的迭代器做
splice,绝不复用其他临时获取的 list 迭代器 - map 插入新节点后,立刻用返回的
pair<iterator bool>::first</iterator>更新 map 值,别用list.begin()这种易变的表达式 - 如果编译器支持,开启
-D_GLIBCXX_DEBUG(GCC)或_ITERATOR_DEBUG_LEVEL=2(MSVC)能捕获大部分迭代器误用










