本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,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)方法的逻辑如图:
核心逻辑
删除某个键key肯定是要同时修改三个映射表的,借助minFreq参数可以从FK表中找到freq最小的keyList,根据时序,其中第一个元素就是要被淘汰的deletedKey,操作三个映射表删除这个key即可。
总结
一般情况下,LFU效率要优LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题,但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即LFU 存在历史数据影响将来数据的缓存污染问题。此算法只是LFU的简单实现,存在的问题是最近加入的数据因为起始的频率很低,所以容易被淘汰。 实际上为了避免早期的热点数据一直占据缓存,即LFU算法也需有一些访问时间模式的特性。所以需要加上时间限制。实际应用过程中可以参考redis的实现方式。