LRU缓存实现
面试官问:"设计一个LRU缓存,支持get和put操作,要求时间复杂度都是O(1)。"
候选人小张回答:"用哈希表存储key-value,get的时候返回value就行了。"
面试官追问:"那怎么淘汰最久未使用的元素?"
小张愣住了...
一、从一个问题开始
LRU(Least Recently Used)缓存是面试中的高频考点,90%的候选人能说出用哈希表实现,但能用O(1)时间复杂度实现淘汰机制的不超过50%。
今天,我们手撕LRU缓存,从设计思路到代码实现。
【直观类比】
LRU缓存就像你手机的照片应用:
- 最近查看的照片放在最前面
- 当你查看某张照片,它会跳到最前面
- 当相册满了,自动删除最久远的照片
这就是"最近使用"的策略。
二、核心数据结构
2.1 为什么需要特殊设计?
普通的哈希表可以做到O(1)的get和put,但无法实现LRU淘汰。
普通链表可以记录访问顺序,但get是O(n)。
解决方案:哈希表 + 双向链表,兼顾查找和顺序。
2.2 双向链表节点定义
2.3 哈希表加双向链表
三、核心操作实现
3.1 get 操作
步骤:1. 查找节点;2. 移动到头部
3.2 put 操作
步骤:1. 查找/创建节点;2. 更新值;3. 移动到头部;4. 必要时淘汰
3.3 辅助方法
3.4 操作图解
四、Java内置实现:LinkedHashMap
4.1 使用LinkedHashMap实现LRU
4.2 LinkedHashMap的访问顺序
4.3 完整实现
五、LFU:最不经常使用
5.1 LFU vs LRU
5.2 LFU实现思路
六、面试高频追问
6.1 追问一:为什么用双向链表而不是单向链表?
因为需要O(1)删除节点。单向链表删除需要找到前驱节点,是O(n)。
6.2 追问二:为什么用虚拟头尾节点?
避免边界判断。如果没有虚拟节点,删除第一个或最后一个节点时需要特殊处理。
6.3 追问三:LRU和LFU的区别和使用场景?
LRU:
- 淘汰最久未使用的
- 适合短期热点数据
- Redis的近似LRU算法
LFU:
- 淘汰使用频率最低的
- 适合长期热点数据
- CDN缓存、CDN推荐用LFU
七、边界与特例
7.1 容量为1
7.2 重复put相同的key
7.3 并发场景
八、常见误区
❌ 误区一:LRU缓存只需要哈希表
实际情况:哈希表可以O(1)查找,但无法O(1)淘汰。需要配合链表。
❌ 误区二:LinkedHashMap默认是LRU
实际情况:默认accessOrder=false,按插入顺序。需要设置accessOrder=true。
❌ 误区三:LRU比LFU好
实际情况:LRU适合短期热点,LFU适合长期热点。选择取决于使用场景。
九、记忆技巧
用一句话记住LRU的实现:
哈希表负责O(1)查找,双向链表负责O(1)顺序,头部是最近,尾部是最久
用口诀记住操作:
get要移动到头,put要判断容量,超容量删尾部节点
十、实战检验
检验一:力扣146题 - LRU缓存
十一、总结
LRU缓存的核心是哈希表加双向链表:
- 哈希表:O(1)查找
- 双向链表:O(1)顺序维护和淘汰
- 头节点:最近使用
- 尾节点:最久未使用
记住这三句话:
- LRU的核心是维护访问顺序,哈希表加链表是最优解
- LinkedHashMap是Java中LRU的偷懒实现
- LRU适合短期热点,LFU适合长期热点
下一篇文章,我们来聊聊布隆过滤器,看看如何用极小的空间判断元素是否存在。