代表性面试主题

如何实现 LRU-K 缓存?

编程题困难
Offer.cc 编辑团队发布 更新

题干

请实现一个容量为 capacity、参数为 K 的 LRU-K 缓存。未达到 K 次访问的条目应优先于有 K 次历史访问的条目淘汰;同一组内按第 K 近访问时间排序,并说明复杂度、并发和测试策略。

题目与使用场景

实现一个固定容量的 LRU-K 缓存,支持 getput 和淘汰。每个键记录最近 K 次访问;访问次数不足 K 次的条目属于历史不足区,应先于“已热”条目淘汰。需要说明 K、容量、更新已有键、并发调用和不存在键的行为。

面试官考察什么

  • 能否准确维护访问历史与两类候选集合。
  • 是否能用堆、哈希表或有序结构维持淘汰顺序,并分析复杂度。
  • 能否处理重复写入、容量为零、K 非法和并发可见性。
  • 是否理解 LRU-K 的目标是过滤一次性扫描,不等于所有工作负载都更快。

作答前的澄清问题

确认是否要求线程安全、是否允许近似淘汰、值是否可变、是否需要 TTL,以及是否要统计命中率。若要求严格顺序,通常需要锁或串行化更新;若追求吞吐,可采用分片和近似策略。

30 秒回答框架

我会为每个键保存值、最近 K 次时间戳和版本。候选分为历史不足区与已热区:容量超限时先淘汰历史不足区中最久未访问者,否则淘汰已热区中第 K 近时间最小者。哈希表负责 O(1) 定位,堆保存候选顺序,更新堆时用版本号丢弃过期节点。严格实现的 getput 期望 O(log n),空间 O(capacity·K)。

分步骤深入解答

1. 记录访问历史

每次命中或写入都追加逻辑时钟值,并只保留最近 K 次。逻辑时钟比墙上时间更适合比较顺序,也避免同一毫秒访问难以区分。已有键的 put 应更新值并计为一次访问,除非题目明确要求写入不计访问。

2. 维护淘汰候选

历史不足区的排序键是最近一次访问时间;已热区的排序键是第 K 近访问时间。两个最小堆分别存 (key, version, rank)。键再次访问时插入新节点并增加版本,弹出时验证版本和当前 rank,失效节点直接跳过。

3. 边界与并发

容量为零或负数时不缓存;K 为零或负数时应拒绝参数。淘汰和值更新必须在同一临界区内完成,避免两个线程同时看到空位而超容量。分片锁能提高吞吐,但跨分片的全局容量需要额外协调。

高质量示范回答

我先把条目分成历史不足和已热两区。每个条目保存值、最近 K 次逻辑时间与版本;访问更新历史后,把新排序节点压入对应最小堆。淘汰时先检查历史不足堆,再检查已热堆,并用版本校验跳过旧节点。这样定位是哈希表 O(1),每次堆操作 O(log n),空间为 O(capacity·K)。测试会覆盖 K=1 与普通 LRU 的等价性、重复访问晋级、一次性扫描、覆盖写、容量零、并发超容量、堆中失效节点和命中率。LRU-K 适合希望降低扫描污染的场景;Redis 的 LRU 是采样近似,PostgreSQL 使用 clock-sweep,不能把三者的复杂度和效果混为一谈。

常见错误

  • 只维护一次访问时间,实际实现成普通 LRU。
  • 把第 K 近访问时间误写成最近一次访问时间。
  • 直接删除堆顶却不处理重复节点,导致错误淘汰或内存泄漏。
  • 允许并发 put 突破容量,或在锁外更新访问历史。
  • 声称 LRU-K 在所有负载下都优于 LRU。

追问及应对

K=1 时应该发生什么?

每个条目第一次访问就进入已热语义,淘汰键按最近一次访问排序,因此退化为普通 LRU 的排序规则。

如何降低堆节点的内存成本?

可用索引和可变堆减少重复节点,或采用分代队列、采样淘汰等近似方案,但要明确顺序从严格变为近似,并重新测试命中率。

如何验证一次性扫描污染是否降低?

构造循环热点集合后插入大批只访问一次的键,比较 LRU 与 LRU-K 的热点命中率、淘汰数量、延迟和内存占用;同时测试热点集合大小接近容量的边界。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具