news 2026/9/4 19:46:44

LRU 缓存原理与实现(让你彻底搞懂什么是最近最少使用)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LRU 缓存原理与实现(让你彻底搞懂什么是最近最少使用)

引言

在计算机里,缓存(Cache)的容量通常是有限的。当缓存满了,再有新数据进来时,就需要淘汰一些旧数据。那到底该淘汰谁呢?这就是一个很现实的问题,也是很多面试官喜欢问的问题。

LRU的全称是Least Recently Used,翻译过来就是最近最少使用。它的核心思想非常简单:

如果一个数据最近被访问过,那么它将来被访问的概率也更高;反过来,很久都没被访问过的数据,大概率以后也不怎么用,可以优先淘汰。

简单点说,LRU 就像我们收拾书桌一样:最久没碰的东西,最应该先被清理掉。

1. 生活中的例子

假设你的书桌只能放 3 本书(缓存容量是 3):

  1. 你先拿了《数据结构》来看。
  2. 然后拿了《操作系统》。
  3. 接着拿了《计算机网络》。

这时候书桌满了。如果你接下来想看《算法》,就必须先拿走一本。按照 LRU 的策略,你会优先拿走最早看的那本《数据结构》,因为它是“最近最少使用”的。

注意这里的关键词是“最近”,而不是“读过一次就不再需要”。比如你刚才还在翻《操作系统》,那么即使《操作系统》比《数据结构》买得更早,它依然会被视为“最近用过”,暂时不会被淘汰。这也是 LRU 和其他简单淘汰策略(比如先进先出)最大的区别。

2. LRU 需要支持的操作

一个 LRU 缓存通常要高效支持两个核心操作:

  • get(key):查询这个 key 对应的值。
    • 如果 key 存在,返回它的值,并且把这个 key 标记为“最近使用过”。
    • 如果 key 不存在,返回 -1。
  • put(key, value):插入或更新一个键值对。
    • 如果 key 已经存在,就更新它的值,并标记为最近使用。
    • 如果 key 不存在,就插入。
    • 如果插入后缓存满了,就要把最久没使用的那个数据淘汰掉。

3. 怎么实现才高效?

如果只用普通数组或链表,查找和移动的效率会比较低。比如:

  • 用数组找某个 key 要遍历,最坏是 O(n)。
  • 用单链表虽然删除方便,但要找到某个节点还是得从头扫描。

最经典的高效实现是:哈希表 + 双向链表

  • 哈希表:用来根据 key 快速找到对应的节点,时间复杂度 O(1)。
  • 双向链表:用来维护数据的使用顺序。
    • 链表头部表示最近使用的。
    • 链表尾部表示最久没使用的。

操作逻辑如下:

  • 每次 get 或 put 一个已存在的 key,就把它移到链表头部。
  • 需要淘汰时,直接删除链表尾部的节点。

这样就能保证所有操作都是 O(1) 的。这也是 LRU 成为面试高频题的原因之一:它把数据结构的组合运用考得很到位。

4. 结构示意图

哈希表负责“快速找到”,双向链表负责“维护顺序”:

哈希表:通过 key 快速定位到链表中的节点。

双向链表:Head ↔ [最近使用] ↔ [次近使用] ↔ ... ↔ [最久没使用] ↔ Tail。

5. 核心操作总结

操作具体动作
get 存在返回值 + 把节点移到头部
get 不存在返回 -1
put 已存在更新值 + 移到头部
put 不存在新建节点放到头部。如果满了,先删尾部再插入

6. 完整 C++ 代码实现(哈希表 + 双向链表)

前面已经把原理讲清楚了,下面直接给出完整可运行的 C++ 代码,并配上详细注释。代码里用到了虚拟头尾节点,这个设计能让新增和删除操作少写很多判断,建议初学者仔细体会。

6.1 完整代码实现

#include <iostream> #include <unordered_map> using namespace std; // 双向链表节点 struct DLinkedNode { int key; int value; DLinkedNode* prev; DLinkedNode* next; DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {} DLinkedNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {} }; class LRUCache { private: unordered_map<int, DLinkedNode*> cache; // 哈希表:key -> 链表节点指针 DLinkedNode* head; // 虚拟头节点 DLinkedNode* tail; // 虚拟尾节点 int capacity; // 缓存容量 int size; // 当前缓存大小 // 将节点添加到双向链表头部 void addToHead(DLinkedNode* node) { node->prev = head; node->next = head->next; head->next->prev = node; head->next = node; } // 删除链表中的某个节点 void removeNode(DLinkedNode* node) { node->prev->next = node->next; node->next->prev = node->prev; } // 将节点移动到头部(先删再加) void moveToHead(DLinkedNode* node) { removeNode(node); addToHead(node); } // 删除尾部节点(最久未使用),并返回该节点方便清理 DLinkedNode* removeTail() { DLinkedNode* node = tail->prev; removeNode(node); return node; } public: LRUCache(int capacity) { this->capacity = capacity; this->size = 0; // 使用虚拟头尾节点,简化边界操作 head = new DLinkedNode(); tail = new DLinkedNode(); head->next = tail; tail->prev = head; } int get(int key) { if (cache.find(key) == cache.end()) { return -1; // key 不存在 } // key 存在,移动到头部表示最近使用 DLinkedNode* node = cache[key]; moveToHead(node); return node->value; } void put(int key, int value) { if (cache.find(key) != cache.end()) { // key 已存在,更新值并移到头部 DLinkedNode* node = cache[key]; node->value = value; moveToHead(node); } else { // key 不存在,创建新节点 DLinkedNode* node = new DLinkedNode(key, value); cache[key] = node; addToHead(node); size++; // 如果超出容量,淘汰尾部节点 if (size > capacity) { DLinkedNode* removed = removeTail(); cache.erase(removed->key); delete removed; size--; } } } // 析构函数,释放内存 ~LRUCache() { DLinkedNode* cur = head; while (cur) { DLinkedNode* next = cur->next; delete cur; cur = next; } } };

6.2 测试代码

int main() { LRUCache cache(2); // 容量为 2 cache.put(1, 1); // 缓存是 {1=1} cache.put(2, 2); // 缓存是 {1=1, 2=2} cout << cache.get(1) << endl; // 返回 1,缓存变成 {2=2, 1=1} cache.put(3, 3); // 淘汰 key 2,缓存是 {1=1, 3=3} cout << cache.get(2) << endl; // 返回 -1(未找到) cache.put(4, 4); // 淘汰 key 1,缓存是 {3=3, 4=4} cout << cache.get(1) << endl; // 返回 -1(未找到) cout << cache.get(3) << endl; // 返回 3 cout << cache.get(4) << endl; // 返回 4 return 0; }

预期输出:

1 -1 -1 3 4

6.3 代码要点说明

  • 虚拟头尾节点:避免在增删头部或尾部节点时做大量判空,代码更简洁,也不容易写出空指针 bug。
  • moveToHead:每次访问或更新节点时调用它,保证头部始终是最新使用的。
  • removeTail:容量满时调用,淘汰最久未使用的节点。删除后别忘了同时清理哈希表中的映射,并释放内存。

初学者可以重点看这里:为什么链表节点的字段里要同时保存keyvalue?原因是在淘汰尾部节点时,我们需要根据这个节点的 key 去哈希表里执行erase。如果节点里不存 key,只存 value,淘汰时就无法准确删除哈希表里的对应项。

7. 为什么面试常考 LRU?

  1. 考查你对哈希表和链表的理解与组合使用。
  2. 考查你对“时间复杂度”的敏感度,能不能做到 O(1)。
  3. 和操作系统、数据库缓存等实际场景联系紧密。

另外,LRU 还有一种更简洁但同样常考的实现方式,就是直接使用 C++ 的listunordered_map组合。不过手写双向链表能够更清楚地展示对指针和节点操作的理解,建议面试前两种方式都准备一下。

8. 总结

LRU 的本质就是:

  • 双向链表维护“谁先谁后被使用”。
  • 哈希表保证查找是 O(1)。
  • 淘汰时总是丢弃最久没被访问的数据。

一句话记住:哈希表负责快速查找,链表负责维护顺序,满了就淘汰链表尾部。

理解了这个思想,后面再看代码实现,就会轻松很多。建议你先把 6.2 节的手动运行过程在纸上模拟一遍,再自己动手写一遍完整代码,这样对 LRU 的理解会非常扎实。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 5:59:13

嵌入式PID三分钟调参实战:从响应曲线快速定位参数问题

你是不是也遇到过这种情况&#xff1a;在智能车、无人机、机器人或者温控项目中&#xff0c;辛辛苦苦写好了PID控制算法&#xff0c;一上电&#xff0c;系统要么纹丝不动&#xff0c;要么疯狂振荡&#xff0c;要么慢得像蜗牛&#xff0c;完全达不到“稳、准、快”的效果。然后&…

作者头像 李华
网站建设 2026/9/4 16:33:06

设备故障预测系统毕业设计:从架构到部署的全流程实战指南

简介&#xff1a;本资源是一套完整的设备故障预测系统毕业设计项目&#xff0c;面向人工智能、自动化、物联网等计算机相关专业的本科生及研究生&#xff0c;解决工业设备运行状态监测与早期故障预警的实际问题&#xff0c;适用于毕业设计、课程设计、项目立项演示及算法学习进…

作者头像 李华
网站建设 2026/9/4 22:25:44

H5代付系统架构:协议适配器模式与多渠道统一调度

简介&#xff1a;最新版H5十四合一代付系统源码是一套面向互联网金融开发者与中小支付服务商的开源代付解决方案&#xff0c;聚焦解决微信生态下域名易被封禁、资金流转稳定性不足及定制化能力弱等核心痛点。资源包共102个文件&#xff0c;含25个PHP后端逻辑文件、25个JPG/PNG图…

作者头像 李华
网站建设 2026/9/5 10:36:16

MATLAB仿真多径衰落信道下OFDM系统:从原理到工程实践

简介&#xff1a;本资源是一套面向通信工程专业本科生及无线通信初学者的MATLAB仿真实验代码&#xff0c;聚焦OFDM系统在多径衰落信道下的建模与性能分析&#xff0c;解决理论学习与实际信道环境脱节的关键问题。压缩包共含4个.m文件&#xff0c;总大小仅2KB&#xff0c;精炼实…

作者头像 李华
网站建设 2026/9/4 21:44:28

AI办公助理会员价值评估:1499元年费是否值得为文档自动化买单?

1. 先搞清楚“办公助理会员”到底卖的是什么看到“付费服务”和“1499元”这个数字&#xff0c;很多人的第一反应是“AI工具也开始收高价会员费了”。但先别急着下结论&#xff0c;这个“办公助理会员”的核心&#xff0c;不是让你去问“今天天气怎么样”或者“帮我写首诗”&am…

作者头像 李华
网站建设 2026/9/5 3:06:52

2026 年 Python 自动化办公实战!告别重复劳动,效率提升 10 倍

自动化解放双手&#xff01;本文涵盖全部场景, 其中包括, Excel自动化, 具体有批量处理、公式、图表&#xff1b;Word自动化涉及 -docx, 包括批量生成报告、合同、标书&#xff1b;PDF 处理包括合并、拆分、提取、转换、加水印&#xff1b;邮件自动化涵盖自动发送、接收、附件&…

作者头像 李华