Java版LeetCode热题100之「LRU 缓存」详解

Java版LeetCode热题100之「LRU 缓存」详解

本文约9200字,全面深入剖析 LeetCode 第146题《LRU 缓存》。涵盖题目解析、哈希表+双向链表解法、复杂度分析、面试高频问答、实际开发应用场景、相关题目推荐等,助你彻底掌握缓存淘汰算法的核心实现技巧。


一、原题回顾

题目描述:
请你设计并实现一个满足LRU (最近最少使用) 缓存约束的数据结构。

实现LRUCache类:

  • LRUCache(int capacity)以正整数作为容量capacity初始化 LRU 缓存
  • int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value)
    • 如果关键字key已经存在,则变更其数据值value
    • 如果不存在,则向缓存中插入该组key-value
    • 如果插入操作导致关键字数量超过capacity,则应该逐出最久未使用的关键字

约束条件:

  • 函数getput必须以O(1) 的平均时间复杂度运行

示例:

输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4

提示:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 10⁵
  • 最多调用2 * 10⁵getput

二、原题分析

LRU(Least Recently Used)缓存是一种经典的缓存淘汰策略,核心思想是:当缓存满时,优先淘汰最久未使用的数据

核心挑战:

  1. 快速查找:需要 O(1) 时间判断 key 是否存在
  2. 快速更新使用顺序:每次访问都要将数据标记为"最近使用"
  3. 快速淘汰:当容量超限时,快速找到并删除最久未使用的数据
  4. O(1) 时间复杂度:所有操作必须在常数时间内完成

关键观察:

  • 哈希表:提供 O(1) 的 key 查找能力
  • 双向链表:提供 O(1) 的节点移动和删除能力
  • 组合使用:哈希表存储 key → 节点的映射,双向链表维护使用顺序

📌为什么需要双向链表?

  • 单向链表无法 O(1) 删除任意节点(需要前驱指针)
  • 双向链表可以 O(1) 删除任意位置的节点

三、答案构思

核心数据结构设计

1. 双向链表节点定义:

classDLinkedNode{intkey;intvalue;DLinkedNodeprev;// 前驱指针DLinkedNodenext;// 后继指针}

2. 整体结构:

  • 头部:最近使用的数据
  • 尾部:最久未使用的数据
  • 哈希表Map<Integer, DLinkedNode>,快速定位节点

3. 操作流程:

get(key) 操作:

  1. 哈希表查找 key
  2. 不存在 → 返回 -1
  3. 存在 → 将对应节点移动到头部 → 返回 value

put(key, value) 操作:

  1. 哈希表查找 key
  2. key 存在
    • 更新 value
    • 移动节点到头部
  3. key 不存在
    • 创建新节点
    • 添加到头部
    • 添加到哈希表
    • 检查容量:
      • 超出 → 删除尾部节点 + 哈希表中对应项

辅助方法设计:

  • addToHead(node):添加节点到头部
  • removeNode(node):删除指定节点
  • moveToHead(node):移动节点到头部(删除 + 添加)
  • removeTail():删除尾部节点并返回

💡伪头尾节点技巧:使用 dummy head 和 dummy tail 简化边界处理


四、完整答案(Java 实现)

/** * LRU 缓存实现 */publicclassLRUCache{/** * 双向链表节点 */classDLinkedNode{intkey;intvalue;DLinkedNodeprev;DLinkedNodenext;publicDLinkedNode(){}publicDLinkedNode(intkey,intvalue){this.key=key;this.value=value;}}// 哈希表:key -> 节点privateMap<Integer,DLinkedNode>cache;// 当前缓存大小privateintsize;// 缓存容量privateintcapacity;// 伪头部和伪尾部节点privateDLinkedNodehead;privateDLinkedNodetail;/** * 构造函数 */publicLRUCache(intcapacity){this.size=0;this.capacity=capacity;this.cache=newHashMap<>();// 初始化伪头尾节点this.head=newDLinkedNode();this.tail=newDLinkedNode();head.next=tail;tail.prev=head;}/** * 获取缓存值 */publicintget(intkey){DLinkedNodenode=cache.get(key);if(node==null){return-1;}// 移动到头部(标记为最近使用)moveToHead(node);returnnode.value;}/** * 插入或更新缓存 */publicvoidput(intkey,intvalue){DLinkedNodenode=cache.get(key);if(node==null){// key 不存在,创建新节点DLinkedNodenewNode=newDLinkedNode(key,value);cache.put(key,newNode);addToHead(newNode);size++;// 检查容量限制if(size>capacity){// 删除尾部节点(最久未使用)DLinkedNodetailNode=removeTail();cache.remove(tailNode.key);size--;}}else{// key 存在,更新值并移动到头部node.value=value;moveToHead(node);}}/** * 添加节点到头部 */privatevoidaddToHead(DLinkedNodenode){node.prev=head;node.next=head.next;head.next.prev=node;head.next=node;}/** * 删除指定节点 */privatevoidremoveNode(DLinkedNodenode){node.prev.next=node.next;node.next.prev=node.prev;}/** * 移动节点到头部 */privatevoidmoveToHead(DLinkedNodenode){removeNode(node);addToHead(node);}/** * 删除尾部节点并返回 */privateDLinkedNoderemoveTail(){DLinkedNoderes=tail.prev;removeNode(res);returnres;}}

五、代码分析

1. 伪头尾节点的作用

// 初始化head=newDLinkedNode();tail=newDLinkedNode();head.next=tail;tail.prev=head;

优势:

  • 避免空指针异常
  • 统一处理插入/删除逻辑
  • 无需特殊处理头尾边界情况

链表结构示意图:

head <-> [最近使用] <-> ... <-> [最久未使用] <-> tail

2. addToHead 方法详解

privatevoidaddToHead(DLinkedNodenode){node.prev=head;node.next=head.next;head.next.prev=node;head.next=node;}

执行步骤:

  1. node.prev = head→ 节点前驱指向 head
  2. node.next = head.next→ 节点后继指向原第一个数据节点
  3. head.next.prev = node→ 原第一个数据节点的前驱指向新节点
  4. head.next = node→ head 的后继指向新节点

效果:新节点成为第一个数据节点

3. removeNode 方法详解

privatevoidremoveNode(DLinkedNodenode){node.prev.next=node.next;node.next.prev=node.prev;}

执行步骤:

  1. 前驱节点的 next 指向后继节点
  2. 后继节点的 prev 指向前驱节点
  3. 被删除节点从链表中断开

关键:双向链表才能实现 O(1) 删除任意节点

4. 容量控制逻辑

if(size>capacity){DLinkedNodetailNode=removeTail();cache.remove(tailNode.key);size--;}

淘汰策略:总是删除 tail.prev(最久未使用的数据)


六、时间复杂度和空间复杂度分析

操作时间复杂度说明
getO(1)哈希表查找 + 链表移动
putO(1)哈希表操作 + 链表操作
总体O(1)满足题目要求

空间复杂度:O(capacity)

  • 哈希表最多存储capacity个键值对
  • 双向链表最多存储capacity个节点
  • 伪头尾节点占用常数空间

完全满足题目要求的 O(1) 时间复杂度!


七、常见问题解答(FAQ)

Q1:为什么不能只用哈希表?


哈希表虽然提供 O(1) 查找,但无法维护使用顺序。
当需要淘汰数据时,无法知道哪个是最久未使用的。
必须配合链表来维护访问顺序。


Q2:为什么不能只用双向链表?


双向链表虽然能维护顺序,但查找特定 key 需要 O(n) 时间遍历。
无法满足 O(1) 查找的要求。
必须配合哈希表来快速定位节点。


Q3:能否用 LinkedHashMap 实现?

:可以,但不符合面试要求

classLRUCacheextendsLinkedHashMap<Integer,Integer>{privateintcapacity;publicLRUCache(intcapacity){super(capacity,0.75F,true);// accessOrder = truethis.capacity=capacity;}@OverrideprotectedbooleanremoveEldestEntry(Map.Entry<Integer,Integer>eldest){returnsize()>capacity;}}

面试官期望:考察你对底层数据结构的理解,而不是 API 调用。


Q4:如果 key 为负数怎么办?


题目已限定0 <= key <= 10000,所以无需考虑负数。
但在实际工程中,应该使用能处理任意整数的哈希表(Java HashMap 支持)。


八、优化思路

1. 线程安全版本

在多线程环境下,需要加锁:

publicsynchronizedintget(intkey){// ...}publicsynchronizedvoidput(intkey,intvalue){// ...}

缺点:性能下降
改进:使用读写锁或分段锁

2. 内存优化

  • 使用LinkedHashMap的内部实现(但面试不推荐)
  • 对于大量小对象,考虑对象池减少 GC 压力

3. 扩展功能

  • TTL(Time To Live):支持过期时间
  • 统计信息:记录命中率、访问次数等
  • 批量操作:支持批量 get/put

4. 工程化增强

// 输入校验publicvoidput(intkey,intvalue){if(capacity<=0){thrownewIllegalStateException("Capacity must be positive");}// ... 原逻辑}// 单元测试@TestvoidtestLRUCache(){LRUCachecache=newLRUCache(2);cache.put(1,1);cache.put(2,2);assertEquals(1,cache.get(1));cache.put(3,3);assertEquals(-1,cache.get(2));}

九、数据结构与算法基础知识点回顾

1. LRU 缓存原理

  • 核心思想:最近使用的数据更可能再次被使用
  • 淘汰策略:当缓存满时,淘汰最久未使用的数据
  • 应用场景:操作系统页面置换、数据库缓存、Web 缓存

2. 哈希表(HashMap)

  • 时间复杂度:平均 O(1) 查找/插入/删除
  • 空间复杂度:O(n)
  • Java 实现:基于数组 + 链表/红黑树

3. 双向链表

  • 特点:每个节点有 prev 和 next 指针
  • 优势:O(1) 插入/删除任意位置
  • 劣势:额外的内存开销(prev 指针)

4. 伪节点技巧(Dummy Node)

  • 作用:简化边界条件处理
  • 应用:链表反转、合并、删除等操作
  • 优势:避免大量 null 检查

十、面试官提问环节(模拟)

❓ 问题1:你的实现中,如果并发调用 get 和 put 会有什么问题?

回答
会出现线程安全问题

  • 哈希表可能处于不一致状态
  • 链表指针可能出现错乱
  • size 计数可能不准确

解决方案

  1. 简单方案:给 get 和 put 方法加 synchronized
  2. 优化方案:使用 ReadWriteLock,get 用读锁,put 用写锁
  3. 高级方案:参考 ConcurrentHashMap 的分段锁思想

❓ 问题2:为什么选择双向链表而不是其他数据结构?

回答
对比几种方案:

数据结构查找删除任意元素移动到头部
数组O(1)O(n)O(n)
单向链表O(n)O(n)O(1)
双向链表O(1)*O(1)O(1)

*配合哈希表实现 O(1) 查找

双向链表是唯一能同时满足 O(1) 删除任意元素和 O(1) 移动到头部的数据结构。


❓ 问题3:如果要求实现 LFU(最不经常使用)缓存,思路是什么?

回答
LFU 需要维护每个 key 的访问频次,比 LRU 更复杂:

核心思路

  1. 哈希表1:key → (value, frequency)
  2. 哈希表2:frequency → 双向链表(存储相同频次的 key)
  3. 维护最小频次 minFreq

操作

  • get:频次+1,从旧频次链表移到新频次链表
  • put:如果容量满,删除 minFreq 链表的尾部

时间复杂度:O(1)(需要精心设计数据结构)


❓ 问题4:你的实现空间复杂度真的是 O(capacity) 吗?

回答
是的。具体分析:

  • 哈希表:最多存储 capacity 个 entry
  • 双向链表:最多存储 capacity 个数据节点 + 2 个伪节点
  • 其他变量:常数空间

所以总空间复杂度是 O(capacity),符合缓存的预期行为。


十一、这道算法题在实际开发中的应用

LRU 缓存是计算机科学中最经典、应用最广泛的算法之一:

1.操作系统 - 页面置换

  • 虚拟内存管理中,当物理内存不足时,需要将某些页面换出到磁盘
  • LRU 是常用的页面置换算法之一
  • 虽然精确 LRU 实现代价高,但有近似算法(如 Clock 算法)

2.数据库系统 - Buffer Pool

  • 数据库将磁盘页缓存在内存中
  • 使用 LRU 或其变种(如 LRU-K)管理缓存页
  • 提高查询性能,减少磁盘 I/O

3.Web 开发 - HTTP 缓存

  • 浏览器缓存、CDN 缓存都使用 LRU 策略
  • Redis、Memcached 等缓存系统内置 LRU 淘汰策略
  • Spring Cache 也支持 LRU 缓存实现

4.移动开发 - 图片缓存

  • Android 的 LruCache 类就是 LRU 缓存的标准实现
  • 用于缓存 Bitmap,避免频繁加载图片导致 OOM
  • Glide、Picasso 等图片加载库都基于 LRU 缓存

💡核心价值:理解时间和空间的权衡,这是系统设计的基础。


十二、相关题目推荐

掌握本题后,可挑战以下 LeetCode 题目:

题号题目关联点
460. LFU 缓存扩展更复杂的缓存策略
155. 最小栈技巧辅助数据结构
225. 用队列实现栈基础数据结构组合
232. 用栈实现队列基础数据结构组合
380. 常数时间插入、删除和获取随机元素应用哈希表+数组
707. 设计链表基础链表操作

建议先掌握基础数据结构操作,再挑战复杂的系统设计题。


十三、总结与延伸

✅ 本题核心收获

  1. LRU 原理:最近最少使用淘汰策略
  2. 数据结构组合:哈希表 + 双向链表的经典应用
  3. O(1) 操作实现:通过精心设计数据结构达到性能要求
  4. 伪节点技巧:简化链表操作的边界处理

🔮 延伸思考

  • 近似 LRU:在大规模系统中,精确 LRU 代价太高,常用近似算法
  • 分布式 LRU:如何在分布式环境中实现一致性 LRU 缓存?
  • 硬件实现:CPU 缓存、TLB 等硬件缓存如何实现 LRU?

🌟 最后建议

  • 手写代码:在白板上完整写出双向链表操作
  • 讲清思路:面试时先解释 LRU 原理,再说明数据结构选择
  • 主动讨论:提出线程安全、扩展性等工程考虑,展现深度

“缓存虽小,学问很大;LRU 之道,在于平衡。”
掌握本题,你就拥有了设计高效缓存系统的基础。继续前行,系统设计之路越走越宽!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1168598.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

02. 配置DNS服务器

配置服务流程&#xff1a; 注意&#xff1a;/etc/hosts 只适合小规模的集群**、静态的主机名解析场景**&#xff0c;在中大型集群或动态环境中存在明显局限。 /etc/hosts 是 Linux/Unix 系统中一个静态的主机名 - IP 地址映射文件 &#xff0c;作用是实现本地主机名解析&…

Gitee的AI战略转型:中国开源生态的智能化跃迁

Gitee的AI战略转型&#xff1a;中国开源生态的智能化跃迁 中国开源生态正在经历一场由AI技术驱动的深刻变革。作为国内领先的代码托管平台&#xff0c;Gitee近期推出的"模力方舟"战略标志着其从基础代码托管服务向AI增强型工程效率平台的全面升级。这一转型不仅体现…

Java版LeetCode热题100之「K 个一组翻转链表」详解

Java版LeetCode热题100之「K 个一组翻转链表」详解 本文约9200字&#xff0c;全面深入剖析 LeetCode 第25题《K 个一组翻转链表》。涵盖题目解析、模拟解法&#xff08;含子链表反转&#xff09;、复杂度分析、面试高频问答、实际开发应用场景、相关题目推荐等&#xff0c;助你…

网络安全检测实战核心技术:从入侵识别到构建主动威胁感知体系

一&#xff0c;网络安全漏洞 安全威胁是指所有能够对计算机网络信息系统的网络服务和网络信息的机密性&#xff0c;可用性和完整性产生阻碍&#xff0c;破坏或中断的各种因素。安全威胁可分为人为安全威胁和非人为安全威胁两大类。 1&#xff0c;网络安全漏洞威胁 漏洞分析的…

2026年网络安全学习路线+自学笔记(超详细) 自学网络安全看这一篇就够了

一、什么是网络安全 网络安全是一种综合性的概念&#xff0c;涵盖了保护计算机系统、网络基础设施和数据免受未经授权的访问、攻击、损害或盗窃的一系列措施和技术。经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”…

03. NFS服务器

NFS 服务介绍 在计算机网络存储领域&#xff0c;NFS&#xff08;Network File System&#xff0c;网络文件系统&#xff09;是一种实现不同计算机之 间文件共享的经典技术。它允许网络中的客户端计算机像访问本地文件一样&#xff0c;透明地访问远程服 务器上的文件资源&#…

Python+Vue的卫生室药店信息管理系 django Pycharm flask

收藏关注不迷路&#xff01;&#xff01;需要的小伙伴可以发链接或者截图给我 项目介绍 随着时代的发展&#xff0c;我国的医疗事业也取得了非常大的成就&#xff0c;很多大型的医院都哦以及实现了现代医疗信息的管理&#xff0c;但是很多卫士室扔采用人工手动的方式对病人和医…

Java版LeetCode热题100之「随机链表的复制」详解

Java版LeetCode热题100之「随机链表的复制」详解 本文约9200字&#xff0c;全面深入剖析 LeetCode 第138题《随机链表的复制》。涵盖题目解析、两种解法&#xff08;哈希表回溯法 & 节点拆分法&#xff09;、复杂度分析、面试高频问答、实际开发应用场景、相关题目推荐等&a…

伺服电机控制:编码器与霍尔传感器揭秘

伺服电机控制中使用编码器和霍尔传感器在原理、精度、成本和应用场景上有显著区别。以下是详细对比&#xff1a;一、霍尔传感器原理基于磁场感应&#xff1a;霍尔传感器检测永磁体&#xff08;转子&#xff09;的磁场变化&#xff0c;当转子磁极经过时&#xff0c;输出脉冲信号…

Python+Vue的超市进销存管理系统 django Pycharm flask

收藏关注不迷路&#xff01;&#xff01;需要的小伙伴可以发链接或者截图给我 项目介绍 超市进销存管理系统是针对超市运营而设计的一套综合管理系统。超市进销存管理系统旨在提高超市的运营效率和管理水平&#xff0c;传统的超市商品管理都是人工手动的方式在进行管理&#xf…

企业级靓车汽车销售网站管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

摘要 随着互联网技术的飞速发展&#xff0c;汽车销售行业逐步向数字化转型&#xff0c;传统的线下销售模式已无法满足现代消费者的需求。消费者对购车体验的便捷性、透明度和个性化服务提出了更高要求&#xff0c;而企业也需要更高效的管理工具来优化库存、客户关系和销售流程。…

(保姆级)自学网络安全超详细学习路线,从青铜到王者的进阶之路_网络安全工程师自学

算上从学校开始学习&#xff0c;已经在网安这条路上走了10年了&#xff0c;无论是以前在学校做安全研究&#xff0c;还是毕业后在百度、360从事内核安全产品和二进制漏洞攻防对抗&#xff0c;我都深知学习方法的重要性。没有一条好的学习路径和好的学习方法&#xff0c;往往只会…

vue基于Python图书销售数据爬取及可视化系统的设计与实现 flask django Pycharm

收藏关注不迷路&#xff01;&#xff01;需要的小伙伴可以发链接或者截图给我 项目介绍 随着互联网技术的飞速发展和普及&#xff0c;人们的消费习惯和购物方式也在发生深刻的变化。在这样的背景下&#xff0c;图书销售数据爬取及可视化系统应运而生。与传统的实体书店相比&…

Java Web 植物健康系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】

摘要 背景相关 随着城市化进程加快和环境污染问题日益突出&#xff0c;植物健康管理成为生态保护与农业可持续发展的重要课题。传统植物健康监测依赖人工观察和实验室分析&#xff0c;效率低下且成本高昂&#xff0c;难以满足现代精准农业的需求。信息化技术的快速发展为植物健…

Python+Vue的全域智慧旅游系统的设计与实现 django Pycharm flask

收藏关注不迷路&#xff01;&#xff01;需要的小伙伴可以发链接或者截图给我 项目介绍 随着时代的发展&#xff0c;越来越多的人喜欢旅游&#xff0c;但是很多时候人们并不知道去哪里旅游且购买门票和预订也需要到地方后才可以购买和预订&#xff0c;为了方便用户更加方便的预…

网络安全从0到1,保姆级学习路线(2026)

网络安全从0到1&#xff0c;保姆级学习路线&#xff08;2026&#xff09; 一、前言&#xff1a;网络安全为什么值得学&#xff1f; 在数字化浪潮下&#xff0c;网络攻击事件频发&#xff08;数据泄露、勒索病毒、APT 攻击等&#xff09;&#xff0c;企业对安全人才的需求缺口持…

网络信息安全工程师 应该怎样入门?需要有什么知识基础?零基础入门到精通,收藏这一篇就够了

网络安全工程师是一个很广泛的职业概称&#xff0c;只要是网络安全相关的从业者&#xff0c;都算是网络安全工程师。网络安全相关岗位很多&#xff0c;有信息安全工程师、渗透测试工程师、应急响应安全工程师、逆向安全工程师、溯源取证安全工程师、安全架构师、恶意软件安全分…

2026 数据库国产化预测:从替代到引领的格局重构与规模爆发

如果说过去五年数据库国产化是 “破冰突围”&#xff0c;那么 2026 年将正式迈入 “深水区攻坚”。作为数字经济的 “地基”&#xff0c;数据库的自主可控程度直接决定了国家数字战略的安全底线。在信创 2.0 战略纵深推进与 AI、云原生技术浪潮的双重驱动下&#xff0c;国产数据…

入门网络安全工程师要学习哪些内容【2026年寒假最新学习计划】

大家都知道网络安全行业很火&#xff0c;这个行业因为国家政策趋势正在大力发展&#xff0c;大有可为!但很多人对网络安全工程师还是不了解&#xff0c;不知道网络安全工程师需要学什么?知了堂小编总结出以下要点。 网络安全工程师是一个概称&#xff0c;学习的东西很多&…

Python+Vue的 青岛健力士商贸有限公司食品质量管理平台 django Pycharm flask

收藏关注不迷路&#xff01;&#xff01;需要的小伙伴可以发链接或者截图给我 项目介绍 随着我国经济的不断发展&#xff0c;我国的主要矛盾已经从人民日益增长的物质文化需要同落后的社会生产之间的矛盾转化为人民日益增长的美好生活需要和不平衡不充分发展之间的矛盾&#xf…