LRU 缓存
LRU(Least Recently Used)缓存淘汰最久未被访问的数据。面试通常要求 get、put 都达到平均 O(1)。
Map 实现
JavaScript 的 Map 会维护插入顺序。访问键时先删除再插入,即可把它移动到“最新”位置。
其中:
可以拆成三步理解:
this.cache.keys()返回一个只遍历 key 的迭代器。Map会按照插入顺序保存 key。.next()取出迭代器的第一个元素,返回形如{ value, done }的对象。.value取出真正的 key。
因此,如果缓存中的 key 顺序是:
那么:
在这个 LRU 实现中,最久未使用的 key 始终位于 Map 的最前面,所以可以用它完成淘汰。Map 为空时,next().value 会是 undefined,但这里因为外层已经判断了 this.cache.size > this.capacity,正常情况下不会出现空 Map 淘汰。
复杂度与经典实现
get、put:平均O(1)。- 空间:
O(capacity)。
不依赖有序 Map 时,经典答案是哈希表加双向链表:哈希表负责定位节点,双向链表负责在 O(1) 内删除和移动节点,链表尾部保存最久未使用项。单链表无法在 O(1) 内删除任意节点,因为还需寻找前驱。
生产缓存通常还需要 TTL、容量统计和淘汰回调。LRU 只按最近访问淘汰,不保证命中率最优,也不适合对象体积差异极大的缓存。
带 TTL 的 LRU Cache
TTL(Time To Live)表示缓存项的存活时间。下面的实现约定:
ttl使用毫秒表示,从put写入或更新时开始计时。get命中过期数据时,先删除数据,再返回-1。get命中未过期数据时,会刷新 LRU 顺序,但不会刷新 TTL。- 采用惰性删除:只有访问到过期数据,或主动调用
clearExpired时才删除过期项。
使用示例:
复杂度
get、put、delete:平均O(1)。clearExpired:O(n),其中n是当前缓存项数量。- 空间:
O(capacity)。
惰性删除不会启动后台定时器,适合简单面试实现。如果要求过期数据必须在 TTL 到期的瞬间自动清除,可以为每个缓存项增加定时器,或者使用“哈希表 + 过期时间最小堆”;后者能更稳定地控制清理成本,但实现复杂度更高。
保护热 key 的分段 LRU
普通 LRU 只看“最近一次访问时间”。一个 key 即使被访问过很多次,只要一段时间没有访问,也可能和普通冷数据一样被淘汰。
可以把缓存分成两个区域:
probation:普通区,新写入的 key 先放这里。protected:保护区,key 在普通区再次命中后晋升到这里。
淘汰时先淘汰普通区,只有普通区为空时才淘汰保护区。这样,偶尔访问一次的冷数据不会挤掉已经多次命中的热 key。
使用示例:
这里的“保护”不是永久保留:保护区容量有限。当保护区已满,晋升新热 key 时,保护区中最久未访问的 key 会降级到普通区,并可能在下一次淘汰时被删除。
复杂度
get、put、晋升和淘汰:平均O(1)。- 空间:
O(capacity)。
这个版本适合表达“访问频率优先、时间顺序次之”的缓存策略;如果要严格按照访问次数淘汰,则应改用 LFU,并维护访问频率和频率桶。

