T## 📌 题目描述
请你设计并实现一个满足 LRU(最近最少使用)缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity)以正整数作为容量capacity初始化 LRU 缓存int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1void put(int key, int value)如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应逐出最久未使用的关键字
函数 get 和 put 必须以 的平均时间复杂度运行。
示例:
输入:
["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]
🧠 思路分析
要求 get 和 put 都是 ,这就排除了遍历的可能。核心矛盾在于:HashMap 能 查找,但没法记录"谁最近用过";链表能维护使用顺序,但没法 定位。两者结合就完美了——HashMap 负责快速查找,双向链表负责维护使用顺序。
设计四个核心操作:
- HashMap:key → Node, 定位节点
- 双向链表:head 端是最近使用,tail 端是最久未使用
- 访问节点时:从原位置摘掉,插到 head 后面(
moveToHead) - 容量满了时:摘掉 tail 前面的节点(
removeTail),同时从 Map 中删除
以 capacity = 2 走一遍示例:
put(1, 1): 新建 node(1,1),放 head 后面
Map: {1 → node(1,1)}
链表: head ↔ [1,1] ↔ tail
put(2, 2): 新建 node(2,2),放 head 后面
Map: {1 → node(1,1), 2 → node(2,2)}
链表: head ↔ [2,2] ↔ [1,1] ↔ tail
get(1): 找到 node(1,1),移到 head 后面,返回 1
链表: head ↔ [1,1] ↔ [2,2] ↔ tail
put(3, 3): 3 不存在,新建 node(3,3),放 head 后面
size=3 > capacity=2,移除 tail 前的 [2,2],从 Map 删除 key=2
Map: {1 → node(1,1), 3 → node(3,3)}
链表: head ↔ [3,3] ↔ [1,1] ↔ tail
get(2): Map 里没有 2,返回 -1
put(4, 4): 4 不存在,新建 node(4,4),放 head 后面
size=3 > capacity=2,移除 tail 前的 [1,1],从 Map 删除 key=1
Map: {3 → node(3,3), 4 → node(4,4)}
链表: head ↔ [4,4] ↔ [3,3] ↔ tail
get(1): Map 里没有 1,返回 -1
get(3): 找到,移到 head 后面,返回 3
get(4): 找到,移到 head 后面,返回 4
🖼️ 图解与执行流程
capacity = 2
put(1,1): head ↔ [1,1] ↔ tail
put(2,2): head ↔ [2,2] ↔ [1,1] ↔ tail
get(1): head ↔ [1,1] ↔ [2,2] ↔ tail → return 1
put(3,3): head ↔ [3,3] ↔ [1,1] ↔ tail → 移除 [2,2]
get(2): Map 无 2 → return -1
put(4,4): head ↔ [4,4] ↔ [3,3] ↔ tail → 移除 [1,1]
get(1): Map 无 1 → return -1
get(3): head ↔ [3,3] ↔ [4,4] ↔ tail → return 3
get(4): head ↔ [4,4] ↔ [3,3] ↔ tail → return 4
双向链表 + 哨兵节点(dummy head/tail)的好处在于:插入和删除不需要判断空指针,所有操作都是统一的 pre.next / next.pre 操作。HashMap 保证了 查找,双向链表保证了 的插入和删除。
💻 核心代码实现
class LRUCache {
HashMap<Integer, Node> maps = new HashMap<>();
int capacity;
int size = 0;
Node head = new Node(); // 哨兵头节点
Node tail = new Node(); // 哨兵尾节点
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.pre = head;
}
// 将节点移到 head 后面(先摘掉再插入)
public void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
// 将节点插入到 head 后面
private void addToHead(Node node) {
node.next = head.next;
node.pre = head;
head.next.pre = node;
head.next = node;
}
// 从链表中摘掉节点
private void removeNode(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
// 移除 tail 前面的节点(最久未使用)
private Node removeTail() {
Node last = tail.pre;
removeNode(last);
return last;
}
public int get(int key) {
Node node = maps.get(key);
if (node != null) {
// 访问了就移到 head 后面,标记为最近使用
moveToHead(node);
return node.val;
} else {
return -1;
}
}
public void put(int key, int value) {
Node node = maps.get(key);
if (node == null) {
// key 不存在:新建节点,加入 Map 和链表
node = new Node(key, value);
maps.put(key, node);
addToHead(node);
size++;
// 超容量就移除最久未使用的节点
if (size > capacity) {
Node tail = removeTail();
maps.remove(tail.key);
size--;
}
} else {
// key 已存在:更新值,移到 head 后面
node.val = value;
removeNode(node);
addToHead(node);
}
}
}
class Node {
int key;
int val;
Node next;
Node pre;
Node() {}
Node(int key, int val) {
this.key = key;
this.val = val;
}
}
代码的核心就是四个链表操作:addToHead、removeNode、moveToHead、removeTail。get 和 put 都是在这四个操作的基础上加上 HashMap 的读写。Node 里保存 key 是为了在 removeTail 时能从 Map 中删除对应的 key——这一点很容易被忽略。
复杂度分析
- 时间复杂度:
get和put均为 ,HashMap 查找 + 链表操作都是常数时间 - 空间复杂度:,HashMap 和链表最多存储 capacity 个节点