146. LRU 缓存
问题描述
设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。要求实现以下功能:
LRUCache(int capacity)初始化缓存容量int get(int key)获取键对应的值(不存在返回 -1)void put(int key, int value)插入或更新键值对(容量满时淘汰最久未使用的键)
示例:
输入
["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); // 淘汰 key 2,缓存 {1=1, 3=3}
lRUCache.get(2); // 返回 -1(已淘汰)
lRUCache.put(4, 4); // 淘汰 key 1,缓存 {4=4, 3=3}
lRUCache.get(1); // 返回 -1
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
算法设计
使用 哈希表 + 双向链表 实现:
- 双向链表:维护访问顺序,头部是最新访问节点,尾部是最久未用节点
- 哈希表:存储键到链表节点的映射,实现 O(1) 访问
核心操作
get操作:- 从哈希表获取节点
- 若存在则移动节点到链表头部
put操作:- 若键存在:更新值并移动节点到头部
- 若键不存在:
- 创建新节点并添加到头部
- 若容量超限:删除尾部节点并移除哈希表对应项
代码实现
import java.util.HashMap;
import java.util.Map;
class LRUCache {
// 双向链表节点类
class DLinkedNode {
int key; // 存储key用于删除尾部节点时移除哈希表对应项
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
// 核心数据结构
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int capacity; // 缓存容量
private int size; // 当前缓存大小
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) {
// 创建新节点
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 {
// 更新已有节点
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;
}
}
算法过程
1. 添加节点到头部:
head node nextNode
[ ] --> [node] --> [ ]
[ ] <-- [node] <-- [ ]
步骤:
node.prev = headnode.next = head.nexthead.next.prev = nodehead.next = node
2. 移除节点:
prevNode node nextNode
[ ] --> [node] --> [ ]
[ ] <-- [node] <-- [ ]
步骤:
node.prev.next = node.nextnode.next.prev = node.prev
复杂度分析
- 时间复杂度:O(1)
get和put操作均通过哈希表定位(O(1))- 链表操作(添加/删除/移动)均为 O(1)
- 空间复杂度:O(capacity)
- 哈希表和链表存储容量级别的元素
测试用例
public static void main(String[] args) {
LRUCache cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1)); // 1(更新使用顺序)
cache.put(3, 3); // 淘汰 key 2
System.out.println(cache.get(2)); // -1(已淘汰)
cache.put(4, 4); // 淘汰 key 1
System.out.println(cache.get(1)); // -1
System.out.println(cache.get(3)); // 3
System.out.println(cache.get(4)); // 4
// 特殊测试:容量为1
LRUCache cache2 = new LRUCache(1);
cache2.put(1, 1);
cache2.put(2, 2); // 淘汰 key 1
System.out.println(cache2.get(1)); // -1
System.out.println(cache2.get(2)); // 2
}
关键点
-
双向链表作用:- 维护
访问顺序(头新尾旧) - 支持 O(1) 时间完成节点移动/删除
- 维护
-
哈希表作用:- 提供 O(1) 的键值访问
- 存储键到链表节点的映射
-
哨兵节点优势:- 简化链表操作(无需判空)
- 统一处理头尾操作
-
节点存储 key 的原因:
- 删除尾部节点时,需通过节点中的 key 删除哈希表对应项
-
操作顺序:
- 添加新节点时:先更新哈希表,再更新链表
- 淘汰节点时:先删除链表节点,再删除哈希表项
常见问题
-
为什么需要双向链表?单链表不行吗?
- 双向链表支持 O(1) 时间删除任意节点(单链表删除节点需遍历找前驱)
-
哈希表为什么存节点而不直接存值?
- 需要通过节点操作链表(移动/删除),仅存值无法定位链表位置
-
如何保证 O(1) 时间复杂度?
- 哈希表保证 O(1)
查找 - 双向链表保证 O(1)
插入/删除
- 哈希表保证 O(1)
-
哨兵节点如何简化操作?
- 确保头尾操作统一(无需特殊处理
空链表或单节点情况)
- 确保头尾操作统一(无需特殊处理
-
为什么节点要存 key?
- 删除尾部节点时,需要同时删除哈希表中对应的键(通过节点中的 key)
1174

被折叠的 条评论
为什么被折叠?



