尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
面试必备:LRU缓存算法深度剖析与手写实现(附完整代码)
面试必备LRU缓存算法深度剖析与手写实现附完整代码引言在计算机系统中缓存无处不在。无论是CPU的L1/L2/L3缓存、数据库的Buffer Pool还是Redis这类内存数据库都在利用缓存来提升数据访问速度。然而缓存空间有限当缓存满了就需要淘汰一些数据以容纳新数据。这时缓存淘汰策略就显得至关重要。在众多淘汰策略中LRULeast Recently Used最近最少使用是面试和实际应用中最常见的一种。它基于“如果数据最近被访问过那么将来被访问的几率也更高”的假设淘汰最长时间未被访问的数据。本文将带你深入理解LRU的思想剖析双向链表哈希表的经典实现并提供完整的可运行代码让你不仅能在面试中应对自如还能真正应用于项目中。核心概念什么是LRULRU算法可以用一句话概括当缓存空间满时优先淘汰最久未被访问的数据。这里的“访问”包括读(get)和写(put)操作。每次访问某个数据该数据就变成了“最近被使用”的从而被移到淘汰队列的最末尾或最前面依实现而定。那些长时间不被访问的数据会逐步被推到“淘汰区”当需要腾出空间时就会被移除。设计需求我们需要实现一个满足如下要求的LRU缓存-get(key)如果key存在返回对应的value并将该数据标记为最近使用如果不存在返回-1或null。-put(key, value)如果key已存在更新value并将其提升为最近使用如果不存在则插入新数据。插入时若缓存已满需要先淘汰最久未使用的数据再插入。- 操作的时间复杂度必须为O(1)。哈希表可以让我们在O(1)时间内完成查找但无法处理“顺序”问题。我们需要一种数据结构能够高效地在头部和尾部进行增删并且能够在O(1)时间内将某个已存在的节点移动到头部或尾部。双向链表Doubly Linked List恰好满足这一点。为什么是双向链表 哈希表双向链表存储实际的数据节点按照访问时间排序。最近访问的节点在链表头部或尾部最久未访问的在另一端。删除节点、在头部插入节点、将任意节点移动到头部这些操作都可以在O(1)完成前提是我们有该节点的引用。哈希表建立key到链表节点的映射使得我们可以O(1)找到节点进而进行后续的指针操作。两者的组合堪称完美哈希表提供快速查找双向链表维护顺序。实战示例手写LRU缓存下面我们用Java来实现一个LRU缓存同样思路也适用于Python文末会提供Python版本。Java实现完整可运行我们设计LRUCache类构造函数传入容量capacity。内部使用HashMap存储key到节点的映射双向链表的头尾哨兵节点简化边界操作。import java.util.HashMap; import java.util.Map; public class LRUCache { // 双向链表节点类 class DLinkedNode { int key; int value; DLinkedNode prev; DLinkedNode next; public DLinkedNode() {} public DLinkedNode(int key, int value) { this.key key; this.value value; } } private MapInteger, DLinkedNode cache new HashMap(); private int size; private int capacity; private DLinkedNode head, tail; // 哨兵节点 public LRUCache(int capacity) { this.size 0; this.capacity capacity; // 初始化头尾哨兵节点并连接 head new DLinkedNode(); tail new DLinkedNode(); head.next tail; tail.prev head; } public int get(int key) { DLinkedNode node cache.get(key); if (node null) { return -1; // 不存在 } // 访问后将该节点移动到头部表示最近使用 moveToHead(node); return node.value; } public void put(int key, int value) { DLinkedNode node cache.get(key); if (node ! null) { // key已存在更新值并移动到头部 node.value value; moveToHead(node); } else { // 创建新节点 DLinkedNode newNode new DLinkedNode(key, value); cache.put(key, newNode); addToHead(newNode); size; if (size capacity) { // 缓存满移除尾部节点最久未使用 DLinkedNode tailNode removeTail(); cache.remove(tailNode.key); size--; } } } // 将节点添加到头部虚拟头节点之后 private void addToHead(DLinkedNode node) { node.prev head; node.next head.next; head.next.prev node; head.next node; } // 从链表中移除某个节点 private void removeNode(DLinkedNode node) { node.prev.next node.next; node.next.prev node.prev; } // 将某个节点移动到头部 private void moveToHead(DLinkedNode node) { removeNode(node); // 先移除 addToHead(node); // 再加到头部 } // 移除尾部节点最久未使用返回被移除的节点 private DLinkedNode removeTail() { DLinkedNode res tail.prev; removeNode(res); return res; } // 测试主函数 public static void main(String[] args) { LRUCache lru new LRUCache(2); lru.put(1, 1); // 缓存: {11} lru.put(2, 2); // 缓存: {11, 22} System.out.println(lru.get(1)); // 返回 1, 缓存: {22, 11}顺序代表最近程度 lru.put(3, 3); // 容量满淘汰key 2缓存: {11, 33} System.out.println(lru.get(2)); // 返回 -1 (不存在) lru.put(4, 4); // 容量满淘汰key 1缓存: {33, 44} System.out.println(lru.get(1)); // -1 System.out.println(lru.get(3)); // 3 System.out.println(lru.get(4)); // 4 } }代码解析- 头节点和尾节点是哨兵节点不存储实际数据这样在添加/删除时不需要判空简化了边界条件。-moveToHead是LRU的核心当一个节点被get或put更新时将其从链表中删除再插入到头部。-removeTail返回尾部的前一个节点即实际尾节点然后从哈希表中删除对应的键。- 所有操作的时间复杂度都是O(1)因为哈希表查找O(1)链表指针调整也是O(1)。Python实现简洁版同样的思路Python中使用内置的OrderedDict可以一行调用move_to_end但面试中通常要求手动实现。以下是一个基于字典和双向链表的Python版本class DLinkedNode: def __init__(self, key0, value0): self.key key self.value value self.prev None self.next None class LRUCache: def __init__(self, capacity: int): self.cache {} self.capacity capacity self.size 0 self.head DLinkedNode() self.tail DLinkedNode() self.head.next self.tail self.tail.prev self.head def get(self, key: int) - int: if key not in self.cache: return -1 node self.cache[key] self._move_to_head(node) return node.value def put(self, key: int, value: int) - None: if key in self.cache: node self.cache[key] node.value value self._move_to_head(node) else: node DLinkedNode(key, value) self.cache[key] node self._add_to_head(node) self.size 1 if self.size self.capacity: removed self._remove_tail() self.cache.pop(removed.key) self.size - 1 def _add_to_head(self, node): node.prev self.head node.next self.head.next self.head.next.prev node self.head.next node def _remove_node(self, node): node.prev.next node.next node.next.prev node.prev def _move_to_head(self, node): self._remove_node(node) self._add_to_head(node) def _remove_tail(self): node self.tail.prev self._remove_node(node) return node常见问题与注意事项1. 为什么要自己实现而不直接使用LinkedHashMapJava或OrderedDictPython面试时面试官希望考察你对数据结构的理解和DIY能力。实际项目中如果语言标准库提供了类似结构直接使用更可靠但理解其内部原理能更好地调试和优化。Java中LinkedHashMap可以设置accessOrdertrue轻松实现LRUPython的OrderedDict配合move_to_end也十分方便。2. 哨兵节点的作用很多初学者容易在头尾节点的空指针问题上犯错。使用哨兵节点后链表始终非空插入和删除操作不需要检查前后节点是否为null代码更加简洁健壮。3. 并发环境下的LRU上述实现都不是线程安全的。如果在多线程环境下使用最简单的方式是在get和put方法上添加synchronized关键字Java或加锁Python。但加锁会降低并发性能。高级方案可以使用分段锁或无锁队列但这已超出本文范围。4. LRU的一些变种LRU-K记录数据最近第K次访问的时间淘汰第K次访问时间最久的数据。比传统LRU更能抵抗“缓存污染”。Two Queues使用两条链表一条FIFO队列接收新数据另一条LRU队列存储再次被访问的数据。ARCAdaptive Replacement Cache同时跟踪LRU和LFU最少使用频率动态调整比例。5. 内存开销每个节点除了存储key和value还额外持有两个指针prev/next有一定内存开销。在实际海量数据的缓存系统中如果无法接受这种开销可以考虑其他紧凑的数据结构或者使用基于时间戳的淘汰策略精度略低但节省空间。总结LRU缓存通过哈希表双向链表的组合完美地实现了O(1)时间复杂度的get和put操作。其核心在于1. 哈希表维护键到节点的映射实现快速定位。2. 双向链表按访问时间排序支持O(1)的节点移动和删除。3. 哨兵节点简化了链表操作。本文提供的Java和Python实现都是面试中的标准答案建议你亲自手写几遍彻底掌握指针操作的细节。最终不仅能在面试中写出来还能清晰地表达设计的原理和边界情况处理这才是真正的掌握。希望这篇文章能帮助你彻底吃透LRU算法让缓存设计不再成为你的软肋。
RELATED

相关推荐

什么叫真正的惊喜?不是模型多,而是接入之后真的省事了

什么叫真正的惊喜?不是模型多,而是接入之后真的省事了

最近看了一下 MaiziTech 旗下的 MaiziAI,第一感觉是:这不是单纯做一个 AI 生图工具,而是更像给开发者、产品团队、AI 工具站准备的“AI 图片/视频模型中台”。 现在市面上 AI 平台很多,Midjourney、OpenAI、Runway、Stable Diffus…

📅 2026/9/29 15:00:00
MonkeyCode私有化部署实战:Docker+WebSocket+Qwen代码智能平台落地指南

MonkeyCode私有化部署实战:Docker+WebSocket+Qwen代码智能平台落地指南

1. 项目概述:MonkeyCode私有化部署到底在解决什么问题?MonkeyCode不是某个具体开源项目的名字,而是对一类“基于大模型能力封装的代码智能辅助平台”的统称——它泛指那些以Qwen系列(尤其是Qwen1.5、Qwen2、Qwen2.5 Code、Qwen3&a…

📅 2026/10/8 4:20:53
Bash 5.2 脚本调试实战:3种方法定位变量未定义与语法错误

Bash 5.2 脚本调试实战:3种方法定位变量未定义与语法错误

Bash 5.2 脚本调试实战:3种方法定位变量未定义与语法错误 调试 Bash 脚本就像侦探破案——你需要收集线索、分析证据,最终找到那个让脚本崩溃的"元凶"。对于中高级开发者来说,简单的 echo 调试早已不够用。本文将带你深入Bash 5…

📅 2026/10/8 19:33:18
MORE NEWS

更多资讯

📰

CentOS 7下Docker Redis三主三从集群一键部署脚本与避坑

简介:针对 CentOS 7.x 环境下希望快速搭建容器化 Redis 集群的 Linux 运维人员与后端开发者,提供一套可传递参数后自动执行的 shell 部署方案,用于减少逐节点配置、容器编排和基础环境准备中的重复操作,适合实验环境验证及中小项目…

📰

E3-1231 v3+B85-D3H黑苹果OpenCore EFI配置与避坑

简介:面向 Intel E3 1231V3 处理器搭配 B85 D3H 主板的黑苹果 EFI 引导配置包,主要服务于希望在非苹果硬件上安装并运行 macOS 的进阶玩家与装机爱好者。内容围绕 Clover/OpenCore 类引导所需组件展开,包含 ACPI 补丁、驱动内核扩展、设备属性…

📰

工业控制系统深度解析:35 电机速度控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Qt飞机大战源码拆解:从工程结构到二次开发实战

简介:这份资源是面向Qt初学者与游戏开发入门者的「飞机大战」完整项目源码包,围绕Qt框架的图形渲染、事件驱动与资源管理等核心能力展开,可用于课程设计、练手项目或Qt技术学习参考。压缩包共223个文件,约6.21MB,以105…

📰

JSP农贸交易系统开发与优化实践

1. 项目概述 这个JSP农贸产品交易系统是一个典型的B2B电子商务平台,专为农产品流通领域设计。我在2018年曾参与过类似项目的全流程开发,深知这类系统对农产品供需匹配的关键作用。系统采用经典的JSPServlet技术栈,配合MySQL数据库&#xff0c…

📰

Qt QTableWidget十万行数据卡顿假死?模型视图优化与排查指南

简介:这份资源面向使用 Qt 进行 GUI 开发、需要解决 QTableWidget 大数据量展示卡顿问题的开发者,核心围绕惰性加载(Lazy Loading)思路展开。包内共 12 个文件,以 5 个 cpp 源文件与 4 个 h 头文件为主体,另…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬