LRU 缓存(LeetCode 146)
medium哈希表双向链表设计
设计思路
LRU (Least Recently Used) 缓存淘汰最近最少使用的条目。
需要同时满足:
get(key)→ O(1):哈希表定位put(key, value)→ O(1):哈希表 + 链表头插
数据结构:哈希表 + 双向链表
- 哈希表:
key → Node,O(1) 查找 - 双向链表:维护访问顺序,头部 = 最近使用,尾部 = 最久未用
实现
class LRUCache {
// 双向链表节点
private static class Node {
int key, val;
Node prev, next;
Node(int k, int v) { key = k; val = v; }
}
private final int capacity;
private final Map<Integer, Node> map = new HashMap<>();
// 哑头/哑尾节点,简化边界处理
private final Node head = new Node(0, 0);
private final Node tail = new Node(0, 0);
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (!map.containsKey(key)) return -1;
Node node = map.get(key);
moveToHead(node); // 访问了,移到头部
return node.val;
}
public void put(int key, int value) {
if (map.containsKey(key)) {
Node node = map.get(key);
node.val = value;
moveToHead(node);
} else {
Node node = new Node(key, value);
map.put(key, node);
addToHead(node);
if (map.size() > capacity) {
Node lru = removeTail(); // 淘汰尾部
map.remove(lru.key);
}
}
}
// ---- 私有辅助方法 ----
private void addToHead(Node node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
private Node removeTail() {
Node lru = tail.prev;
removeNode(lru);
return lru;
}
}
Java 内置 LinkedHashMap 实现(可用作参照,但需掌握底层实现)
class LRUCacheLinkedHashMap extends LinkedHashMap<Integer, Integer> {
private final int capacity;
LRUCacheLinkedHashMap(int capacity) {
super(capacity, 0.75f, true); // accessOrder = true
this.capacity = capacity;
}
public int get(int key) { return super.getOrDefault(key, -1); }
public void put(int key, int value) { super.put(key, value); }
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
底层要求:需要手写双向链表 + 哈希表实现,不能直接使用 LinkedHashMap。
LFU 缓存(LeetCode 460)
LFU 淘汰使用频率最低的(频率相同时淘汰最久未访问的)。
需要额外维护:
keyToVal: key → valuekeyToFreq: key → countfreqToKeys: count → LinkedHashSet(有序集合保留插入序)minFreq: 当前最小频率
class LFUCache {
private final int cap;
private int minFreq = 0;
private final Map<Integer, Integer> keyToVal = new HashMap<>();
private final Map<Integer, Integer> keyToFreq = new HashMap<>();
private final Map<Integer, LinkedHashSet<Integer>> freqToKeys = new HashMap<>();
public LFUCache(int capacity) { cap = capacity; }
public int get(int key) {
if (!keyToVal.containsKey(key)) return -1;
increaseFreq(key);
return keyToVal.get(key);
}
public void put(int key, int value) {
if (cap <= 0) return;
if (keyToVal.containsKey(key)) {
keyToVal.put(key, value);
increaseFreq(key);
} else {
if (keyToVal.size() >= cap) removeMinFreqKey();
keyToVal.put(key, value);
keyToFreq.put(key, 1);
freqToKeys.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);
minFreq = 1;
}
}
private void increaseFreq(int key) {
int freq = keyToFreq.get(key);
keyToFreq.put(key, freq + 1);
freqToKeys.get(freq).remove(key);
if (freqToKeys.get(freq).isEmpty()) {
freqToKeys.remove(freq);
if (minFreq == freq) minFreq++;
}
freqToKeys.computeIfAbsent(freq + 1, k -> new LinkedHashSet<>()).add(key);
}
private void removeMinFreqKey() {
LinkedHashSet<Integer> keys = freqToKeys.get(minFreq);
int evict = keys.iterator().next(); // 最早插入的
keys.remove(evict);
if (keys.isEmpty()) freqToKeys.remove(minFreq);
keyToVal.remove(evict);
keyToFreq.remove(evict);
}
}