LFU缓存(O(1)实现)
LFU的淘汰规则:访问次数最少的先淘汰,次数相同则淘汰最久没用的。
核心问题:怎么做到O(1)?
如果每次淘汰都遍历所有key找最小频次,是O(n),太慢。
关键在于维护一个 minFreq 变量,随时知道当前最小频次是多少,淘汰时直接定位,不用遍历。
三个数据结构:
keyToVal :key → value
keyToFreq :key → 访问次数
freqToKeys :访问次数 → 该次数下所有key(按访问时间排序)
freqToKeys 的value用 LinkedHashSet,原因是:
同频次下要淘汰最久没用的,LinkedHashSet按插入顺序排,头部就是最旧的
还能O(1)删除任意元素
minFreq怎么维护?
只有两种情况需要更新:
put新key时 → 新key频次必为1,所以 minFreq = 1
某个key频次+1,旧桶变空,且旧桶就是minFreq → minFreq++
其他情况不用动,这是整个方案的精髓。

完整代码:
java
class LFUCache {
int cap, minFreq;
Map<Integer, Integer> keyToVal;
Map<Integer, Integer> keyToFreq;
Map<Integer, LinkedHashSet<Integer>> freqToKeys;
public LFUCache(int capacity) {
this.cap = capacity;
keyToVal = new HashMap<>();
keyToFreq = new HashMap<>();
freqToKeys = new HashMap<>();
}
public int get(int key) {
if (!keyToVal.containsKey(key)) return -1;
increaseFreq(key);
return keyToVal.get(key);
}
public void put(int key, int val) {
if (keyToVal.containsKey(key)) {
keyToVal.put(key, val);
increaseFreq(key);
return;
}
if (keyToVal.size() >= cap) {
removeMinFreq();
}
keyToVal.put(key, val);
keyToFreq.put(key, 1);
freqToKeys.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);
minFreq = 1;
}
// key的频次+1,更新三个map
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);
}
//等同于下方freqToKeys.computeIfAbsent
/**if (!freqToKeys.containsKey(freq + 1)) {
freqToKeys.put(freq + 1, new LinkedHashSet<>());
}
freqToKeys.get(freq + 1).add(key);*/
// 淘汰minFreq桶里最旧的key
private void removeMinFreq() {
LinkedHashSet<Integer> keys = freqToKeys.get(minFreq);
int evictKey = keys.iterator().next();
keys.remove(evictKey);
if (keys.isEmpty()) freqToKeys.remove(minFreq);
keyToVal.remove(evictKey);
keyToFreq.remove(evictKey);
}
}
一句话总结:
用 minFreq 快速定位最小频次,用 LinkedHashSet 在同频次内实现LRU兜底,两者结合实现全程O(1)。
难度等级:⭐️⭐️⭐️⭐️⭐️⭐️再加半颗