460. LFU 缓存

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)。

难度等级:⭐️⭐️⭐️⭐️⭐️⭐️再加半颗

相关推荐
MC皮蛋侠客1 天前
Redis 系列(八):缓存设计模式与一致性——从 Cache Aside 到防雪崩
redis·缓存·设计模式
2401_894915531 天前
部署 GEO 优化源码常见报错排查:端口、伪静态、缓存问题解决
java·运维·服务器·后端·缓存·开源
MC皮蛋侠客1 天前
Redis 系列(三):底层实现(一)——对象系统、SDS 与 dict
数据库·redis·缓存
范什么特西2 天前
回答知识总结04(redis)
数据库·redis·缓存
math_hongfan3 天前
鸿蒙离线数据缓存高级架构:弱网预加载/离线数据优先级/同步冲突解决/上线后数据合并策略
学习·缓存·华为·架构·harmonyos·鸿蒙
海上小飞龙3 天前
Redis 持久化:RDB 与 AOF
数据库·redis·缓存
math_hongfan3 天前
鸿蒙存储碎片高级清理机制:数据库Vacuum/文件碎片整理/缓存过期清理/主动空间回收策略
jvm·数据库·学习·缓存·华为·harmonyos·鸿蒙
范什么特西3 天前
知识总结04(redis)
数据库·redis·缓存
無a伟3 天前
redis缓存完整详解
数据库·redis·缓存
敲上瘾4 天前
redis常用数据类型与操作方法
数据结构·数据库·redis·缓存