不能只用 map 实现 lru,因为 map 虽支持 o(1) 查找,但无法在 o(1) 内定位最久未用 key;遍历找最老 key 是 o(n),违反题设要求;标准解法需双向链表 + map 协同维护时序与定位。

为什么不能只用 map 实现 LRU?
因为 map 能做到 Get 和 Put 的 O(1) 查找,但无法记录“访问顺序”——你没法在常数时间内知道哪个 key 是“最久没被访问”的。一旦缓存满,遍历 map 找最老的 key 是 O(N),直接违反题目要求(LeetCode 146 明确要求所有操作 O(1))。
常见错误是:用 map[key]int + 时间戳字段,每次 Get 更新时间戳,Put 满时遍历找最小时间戳。这在小数据量下看似可行,但一到高并发或大容量场景,性能断崖式下跌。
双向链表 + map 组合才是标准解法
核心在于分工明确:map[int]*Node 提供 O(1) 定位,Node 本身串成双向链表维护时序。头部是最近使用,尾部是最久未用,哨兵节点 head 和 tail 简化边界处理。
-
Get存在时调用moveToHead:先removeNode,再addToHead -
Put新 key 且满容量时,必须先removeTail,再delete(cache, removed.key) - 注意:
removeTail返回的是真实尾节点(tail.prev),不是哨兵tail本身
用 container/list 能省事,但有隐藏代价
Go 标准库 container/list 确实封装了双向链表操作,MoveToFront、PushFront、Back 都可用,代码量少很多。但它内部用 *list.Element 做映射,而 Element.Value 是 interface{},每次取值都要类型断言,带来 runtime 开销和 panic 风险。
典型坑点:
- 忘记在
Get里对elem.Value做.(*entry).value断言,直接 panic -
storagemap 存的是*list.Element,但Element不包含 key 字段,删尾时得靠elem.Value.(*entry).key反查,多一次解包 - 相比手写
Node结构体,内存分配更分散,GC 压力略高
初始化和边界条件最容易漏掉
哨兵节点不是可选优化,而是必须项。不设 head 和 tail,addToHead 和 removeTail 就要反复判空,逻辑爆炸。
关键初始化步骤必须一次性做完:
-
head.next = tail和tail.prev = head缺一不可,否则链表断裂 -
cachemap 必须make(map[int]*Node),不能留为 nil,否则put时cache[key] = newNodepanic - 容量为 0 时,
Put应该直接丢弃(不插入),但很多实现忘了判断,导致 map 泄漏
真正难的不是链表操作本身,而是把 map 和链表的生命周期完全对齐——每个 node 插入链表的同时必须进 map,删除时必须同步从 map 和链表里移除。错一步,缓存就不可靠。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











