LFU 缓存淘汰算法

发布时间 2023-06-29 00:01:13作者: ylyzty

Least Frequently Used(LFU): 淘汰使用次数最少的数据。

LFU 算法的需求

  1. 接收一个参数 capacity 作为缓存的最大容量
  2. 实现一个函数 put() 添加数据到缓存
  3. 实现一个函数 get() 查询缓存中的数据
  4. 以上函数应该在 \(O(1)\) 的时间内完成

LFU的算法需求和 LRU算法 非常相似,只是对于节点的顺序上略微不同。对于LRU,最近使用的数据是“最有用的”,插入或更新到队尾;对于LFU,则是使用次数最多的数据是“最有用的”。

LFU 算法的实现

/**
 * LFU 缓存算法
 */
public class LFUCache {
    /**
     * 相同的频次,可能对应多个不同的 key
     * 对于频次相同的 key 应该存在时序关系,即按照从旧到新排序
     * LinkedHashSet: 基于 LinkedHashMap 实现, 哈希 + 双链表 
     */
    private Map<Integer, LinkedHashSet<Integer>> freqToKey;
    private Map<Integer, Integer> keyToFreq;
    private Map<Integer, String> keyToData;
    private int minFreq;
    private int capacity;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.minFreq = 0;

        this.keyToData = new HashMap<>();
        this.keyToFreq = new HashMap<>();
        this.freqToKey = new HashMap<>();
    }

    public String get(int key) {
        if (!keyToData.containsKey(key)) {
            return null;
        }

        increaseFreq(key);
        return keyToData.get(key);
    }

    public void put(int key, String data) {
        // 1. key 已存在, 删除重新添加
        // 2. 容量已满, 删除最少最久的 key
        if (this.capacity <= 0) {
            return;
        }

        if (keyToData.containsKey(key)) {
            keyToData.put(key, data);
            increaseFreq(key);
            return;
        }

        if (this.capacity <= keyToData.size()) {
            removeMinFreqKey();
        }

        keyToData.put(key, data);
        keyToFreq.put(key, 1);
        freqToKey.putIfAbsent(1, new LinkedHashSet<>());
        freqToKey.get(1).add(key);
        this.minFreq = 1;
    }

    private void increaseFreq(int key) {
        int freq = keyToFreq.get(key);
        keyToFreq.put(key, freq + 1);

        freqToKey.get(freq).remove(key); 
        // 检验 freq 对应的 HashSet 是否为空
        if (freqToKey.get(freq).isEmpty()) {
            freqToKey.remove(freq);
            if (freq == this.minFreq) {
                this.minFreq += 1;
            }
        }

        // 检验 freq + 1 对应的 HashSet 是否存在
        freqToKey.putIfAbsent(freq + 1, new LinkedHashSet<>());
        freqToKey.get(freq + 1).add(key);
    }

    
    private void removeMinFreqKey() {
        LinkedHashSet<Integer> keyList = freqToKey.get(minFreq);

        int deleteKey = keyList.iterator().next();
        keyList.remove(deleteKey);

        if (keyList.isEmpty()) {
            /**
             * 无需更新 minFreq, 因为 removeMinFreqKey 函数是由插入新key, 空间不足导致的
             * 插入新key后, minFreq 自然就更新为1, 所以这里无需更新
             */
            freqToKey.remove(minFreq);
        }

        keyToData.remove(deleteKey);
        keyToFreq.remove(deleteKey);
    }
}