真实面经题目 · 原创解析
如何手写 LRU 缓存?
这题是典型手写实现题,关键是用 HashMap 加双向链表在 O(1) 完成查询、更新、移动和淘汰,并处理已有 key、容量边界和 map/list 同步。
真实面经题目 · 原创解析
这题是典型手写实现题,关键是用 HashMap 加双向链表在 O(1) 完成查询、更新、移动和淘汰,并处理已有 key、容量边界和 map/list 同步。
LRU 缓存要让 `get`、`put`、淘汰都接近 O(1)。标准做法是 HashMap 保存 `key -> node`,双向链表按访问新旧排序,头部是最近使用,尾部是最久未使用。`get` 命中后把节点移动到头部;`put` 已有 key 时更新值并移动到头部;`put` 新 key 时插入头部,若超过容量就删除尾部节点并同步删除 map。实现时用 dummy head/tail 简化边界,拆出 `removeNode`、`addFirst`、`moveToHead`、`removeLast`,避免指针错误。
`get(key)` 命中返回值并刷新最近使用顺序,未命中返回约定哨兵值;`put(key, value)` 插入或更新缓存,并在容量超限时淘汰最久未使用项。
HashMap 负责 O(1) 找节点,双向链表负责 O(1) 删除任意节点并插入头部。单链表删除任意节点需要前驱,数组移动元素会退化到 O(n)。
使用虚拟 head 和 tail,真实节点永远在两者之间。插入头部和删除尾部都不用单独判断空链表、首节点、尾节点,代码更稳定。
已有 key:改 value,moveToHead,不增加 size。新 key:创建节点,放入 map,addFirst,size 增加;超过 capacity 时 removeLast,并用被淘汰节点的 key 删除 map。
capacity 小于等于 0 时可以直接不缓存或构造时报错;capacity 为 1 要验证反复淘汰。普通手写版本不是线程安全的,并发场景要加锁、分段或使用成熟缓存库。
class LRUCache {
private static class Node {
int key;
int value;
Node prev;
Node next;
Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private final int capacity;
private final java.util.Map<Integer, Node> map = new java.util.HashMap<>();
private final Node head = new Node(0, 0);
private final Node tail = new Node(0, 0);
LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
int get(int key) {
Node node = map.get(key);
if (node == null) return -1;
moveToHead(node);
return node.value;
}
void put(int key, int value) {
if (capacity <= 0) return;
Node node = map.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
return;
}
Node created = new Node(key, value);
map.put(key, created);
addFirst(created);
if (map.size() > capacity) {
Node removed = removeLast();
map.remove(removed.key);
}
}
private void moveToHead(Node node) {
remove(node);
addFirst(node);
}
private void addFirst(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
private void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private Node removeLast() {
Node node = tail.prev;
remove(node);
return node;
}
} HashMap 能 O(1) 查找,但不知道谁最久未使用。必须额外维护访问顺序,才能快速淘汰。
命中节点后要从原位置删除,单链表需要找到前驱,无法稳定 O(1);双向链表节点自带 prev,可直接断链。
通常算。`put` 已有 key 要更新 value 并移动到头部,否则最近使用语义不完整。
手写结构需要互斥锁保护 map 和链表的一致性。高并发生产场景更常用分段锁或 Caffeine 这类成熟缓存库。