#146
中等
高频
LRU 缓存模拟LRU缓存机制
这是一道围绕设计、哈希表、链表展开的高频练习。建议先掌握「LRU 缓存模拟」这套写法,再结合下方步骤讲解理解状态维护、边界处理和复杂度取舍。
设计
哈希表
链表
双向链表
题目分析
你要设计一个固定容量的缓存。
当缓存没满时,可以继续插入;当缓存满了,再插入新元素时,必须把最近最少使用的那个元素淘汰掉。
它要支持两个操作:
get(key):查到就返回值,否则返回-1put(key, value):插入或更新键值对
只要一个元素被访问过,它就应该被视为“最近使用”。
一句话概括:
设计一个支持 O(1) 查找、更新、淘汰最久未使用元素的缓存结构。
接下来怎么看推荐代码: 带着这个理解再看推荐代码时,重点观察这条主线:用哈希表定位节点,用双向链表维护最近使用顺序,访问和写入都把节点移动到头部。
推荐代码
推荐解法:LRU 缓存模拟
时间复杂度: O(1)
空间复杂度: O(capacity)
核心思路: 用哈希表定位节点,用双向链表维护最近使用顺序,访问和写入都把节点移动到头部。
class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {
}
public DLinkedNode(int _key, int _value) {
key = _key;
value = _value;
}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
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;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
} else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
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;
}
}
结构化讲解
面试时怎么讲
开场思路
这题我会用哈希表加双向链表,把查找和顺序维护拆开处理。
核心过程
- 哈希表负责 key 到节点的 O(1) 定位。
- 双向链表头部表示最近使用,尾部表示最久未使用。
- 每次
get或命中put,都把节点移到头部。 - 插入新节点导致超容量时,淘汰尾部节点并同步删除哈希表记录。
复杂度总结
单次 get / put 时间复杂度 O(1),空间复杂度 O(capacity)。
面试补一句:LRU 真正考的是“两个数据结构如何维持同一个状态”。
核心思路
LRU Cache 的难点在于同时满足两件事:按 key 快速找到节点,以及按最近使用顺序快速淘汰节点。哈希表和双向链表正好一快一序。
步骤讲解
1
哈希表和双向链表配合建模
哈希表负责从 key 直达节点,双向链表负责维护从“最近使用”到“最久未使用”的顺序。
为什么这样做:只靠哈希表无法淘汰最旧节点,只靠链表又做不到 O(1) 查找。
对应代码提示:Map<Integer, Node> cache = new HashMap<>();
2
访问或更新后都移动到头部
只要一个节点被 get 或 put 访问,就把它移到链表头,表示它是最近刚使用过的。
为什么这样做:LRU 的核心就是“最近使用的留下,最久未使用的靠后”。
对应代码提示:remove(node); addToHead(node);
3
容量超限时淘汰尾部节点
插入新节点后,如果容量超出限制,就删除链表尾部前一个真实节点,并同步从哈希表移除。
为什么这样做:尾部节点就是当前最久未使用的那个元素。
对应代码提示:Node removed = removeTail(); cache.remove(removed.key);
易错点
只用哈希表,不维护使用顺序
这样虽然能 O(1) 找 key,但无法 O(1) 知道该淘汰谁。
正确理解:必须配合双向链表维护最近使用次序。
更新已有 key 时没移动到头部
这会导致刚访问过的节点仍然按旧顺序被淘汰。
正确理解:
get 和命中的 put 都要执行“移到头部”。淘汰节点时只删链表不删哈希表
缓存状态会不一致,后续查找可能拿到已经淘汰的节点。
正确理解:淘汰动作必须同时更新链表和哈希表。
复杂度与适用判断
时间复杂度:O(1)
空间复杂度:O(capacity)
比其他方案更好在哪里:比仅用数组或普通链表模拟更高效,满足题目对
O(1) 操作的硬要求。适用判断:只要题目同时要求“按 key 快速访问”与“按最近使用顺序淘汰”,优先想到哈希表 + 双向链表。
额外提醒
- 访问更新头部,淘汰删除尾部,是整个结构的两个核心动作。
动画演示
如果你还没完全看懂,可以展开动画演示。
LRU 缓存模拟动画演示
想看这段解法是怎么一步步运行的,可以展开动画继续观察。
LRU 缓存模拟动画演示
想看这段解法是怎么一步步运行的,可以展开动画继续观察。
当前操作: 无
LRU缓存可视化
容量: 3 | 当前大小: 0
Head
缓存为空
Tail
操作日志
动画演示仅在桌面端提供,移动端请优先阅读推荐代码与结构化讲解。