LFU算法

1,956 阅读3分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

LFU是什么

LFU (Least Frequently Used)最不经常使用即淘汰访问频率最低的元素。

LFU 和 LRU 的区别,LRU 的淘汰规则是基于访问时间,而 LFU 是基于访问次数。

其思想依据是:如果数据最近被访问过,那么将来被访问的几率也更高。

LFU实现

使用一个HashMap存储key到val的映射,就可以快速计算get(key)。

使用一个HashMap存储key到freq的映射,就可以快速操作key对应的freq。

需要freq到key的映射,用来找到freq最小的key。可能有多个key拥有相同的freq,所以freq对key是一对多的关系,即一个freq对应一个key的列表。

希望freq对应的key的列表是存在时序的,便于快速查找并删除最旧的key。

希望能够快速删除key列表中的任何一个key,因为如果频次为freq的某个key被访问,那么它的频次就会变成freq+1,就应该从freq对应的key列表中删除,加到freq+1对应的key的列表中。

这里需要用到LinkedHashSet,LinkedHashSet链表和哈希集合的结合体。链表不能快速访问链表节点,但是插入元素具有时序;哈希集合中的元素无序,但是可以对元素进行快速的访问和删除。

LinkedHashSet兼具了哈希集合和链表的特性,既可以在 O(1) 时间内访问或删除其中的元素,又可以保持插入的时序

class LFUCache {
    // get(), put()方法计数
    Map<Integer, Integer> map;
    Map<Integer, Integer> keyCountMap;
    Map<Integer, LinkedHashSet<Integer>> countToKeysMap;
    int capacity;
    int min;
    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        this.keyCountMap = new HashMap<>();
        this.countToKeysMap = new HashMap<>();
        this.min = 1;
    }
    
    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        int val = map.get(key);
        int oldCount = keyCountMap.get(key);
        int newCount = oldCount + 1;
        // update
        keyCountMap.put(key, newCount);
        if (!countToKeysMap.containsKey(newCount)) {
            countToKeysMap.put(newCount, new LinkedHashSet<>());
        }
        countToKeysMap.get(newCount).add(key);
        // delete
        countToKeysMap.get(oldCount).remove(key);
        if (countToKeysMap.get(oldCount).size() == 0) {
            if (min == oldCount) {
                min = newCount;
            }
            countToKeysMap.remove(oldCount);
        }
        return val;
    }
    
    public void put(int key, int value) {
        if (capacity == 0) {
            return;
        }
        if (map.containsKey(key)) {
            map.put(key, value);
            get(key);
        } else {
            if (map.size() == capacity) {
                int evit = countToKeysMap.get(min).iterator().next();
                map.remove(evit);
                keyCountMap.remove(evit);
                countToKeysMap.get(min).remove(evit);
                if (countToKeysMap.get(min).size() == 0) {
                    countToKeysMap.remove(min);
                }
            }
            min = 1;
            countToKeysMap.putIfAbsent(min, new LinkedHashSet<>());
            countToKeysMap.get(min).add(key);
            map.put(key, value);
            keyCountMap.put(key, 1);
        }
    }
}

put(key, val)方法的逻辑如图:

put.PNG

核心逻辑

删除某个键key肯定是要同时修改三个映射表的,借助minFreq参数可以从FK表中找到freq最小的keyList,根据时序,其中第一个元素就是要被淘汰的deletedKey,操作三个映射表删除这个key即可。

总结

一般情况下,LFU效率要优LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题,但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即LFU 存在历史数据影响将来数据的缓存污染问题。此算法只是LFU的简单实现,存在的问题是最近加入的数据因为起始的频率很低,所以容易被淘汰。 实际上为了避免早期的热点数据一直占据缓存,即LFU算法也需有一些访问时间模式的特性。所以需要加上时间限制。实际应用过程中可以参考redis的实现方式。