LFU
date: 2020-12-08 10:49:21
updated: 2020-12-08 14:57:09
LFU
LFU(Least Frequently Used ,最近最少使用算法)
算法描述:
class LFUCache {
// 构造容量为 capacity 的缓存
public LFUCache(int capacity) {}
// 在缓存中查询 key
public int get(int key) {}
// 将 key 和 val 存入缓存
public void put(int key, int val) {}
}
get(key)
方法会去缓存中查询键key
,如果key
存在,则返回key
对应的val
,否则返回 -1。
put(key, value)
方法插入或修改缓存。如果key
已存在,则将它对应的值改为val
;如果key
不存在,则插入键值对(key, val)
。
当缓存达到容量capacity
时,则应该在插入新的键值对之前,删除使用频次(后文用freq
表示)最低的键值对。如果freq
最低的键值对有多个,则删除其中最旧的那个。
思路:
- 需要有一个HashMap来保存key到val的映射,用来计算
get(key)
HashMap<Integer, Integer> keyToVal;
- 需要有一个HashMap来保存
key
到freq
的映射,用来计算每一个key的频次
HashMap<Integer, Integer> keyToFreq;
-
算法的核心需求(思路)
- 需要
freq
到key
的映射,用来找到最小freq
对应的key
- 如果要满足上一条,快速找到最小
freq
是多少,可以通过一个变量minFreq
来记录当前最小freq
,避免遍历 - 可能会有多个
key
拥有相同的freq
,所以freq
和key
是一对多的关系,那么就需要维护一个freq
到key对应的映射关系 - 为了保证快速查找并删除最旧的
key
,就需要保证freq
对应的key
的映射列表应该是有顺序的 - 需要能够快速删除
key
列表中的任何一个key。如果频次为freq
的key
被访问了,它的频次应该变成freq+1
,此时应该从freq对应的key
列表中删除,并把key
加入到freq+1
对应的key列表中 => 就是需要提高它的频次
HashMap<Integer, LinkedHashSet<Integer>> freqToKeys; int minFreq;
- 需要
如果用 LinkedList 可以满足第3、4条,但是链表不能快速访问某一个节点,所以不能满足第5条快速删除
综上,LFUCache
类代码如下:
class LFUCache {
// key 到 val 的映射,我们后文称为 KV 表
HashMap<Integer, Integer> keyToVal;
// key 到 freq 的映射,我们后文称为 KF 表
HashMap<Integer, Integer> keyToFreq;
// freq 到 key 列表的映射,我们后文称为 FK 表
HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
// 记录最小的频次
int minFreq;
// 记录 LFU 缓存的最大容量
int cap;
public LFUCache(int capacity) {
keyToVal = new HashMap<>();
keyToFreq = new HashMap<>();
freqToKeys = new HashMap<>();
this.cap = capacity;
this.minFreq = 0;
}
public int get(int key) {
if (!keyToVal.containsKey(key))
return -1;
// 增加key对应的频次
increaseFreq(key);
return keyToVal.get(key);
}
public void put(int key, int val) {
// 避免初始化的时候capacity参数异常
if (this.cap <= 0)
return;
// 如果 KV 表已经存在这个key
// 修改对应的val,频次+1
if (keyToVal.containsKey(key)) {
keyToVal.put(key, val);
increaseFreq(key);
return;
}
// 如果 KV 表不存在这个key
// 并且当前容量已满,就需要删除最小频次的key
if (keyToVal.size() >= this.cap) {
removeMinFreqKey();
}
// 当前容量未满,插入key和val,并且key对应的freq置为1
keyToVal.put(key, val);
keyToFreq.put(key, 1);
// putIfAbsent 如果有这个key的话就不进行任何操作,此处相当于对1这个key进行初始化
freqToKeys.putIfAbsent(1, new LinkedHashSet<>());
freqToKeys.get(1).add(key);
// 插入最新的key之后,最小的freq肯定是1
this.minFreq = 1;
}
private void increaseFreq(int key) {
// 首先,能进入到这个函数的时候,keyToFreq 和 freqToKeys 一定保存过这个key了
// 更新 KF 表中key对应的频次+1
int freq = keyToFreq.get(key);
keyToFreq.put(key, freq + 1);
// Fk 表中删除freq对应的列表中的key
freqToKeys.get(freq).remove(key);
freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<>());
freqToKeys.get(freq + 1).add(key);
// 如果 freq 对应的列表空了,就移除这个freq
if (freqToKeys.get(freq).size() == 0) {
freqToKeys.remove(freq);
// 如果这个 freq 又恰好是 minFreq,更新 minFreq
// 这个地方容易忘记更新
if (freq == this.minFreq) {
this.minFreq++;
}
}
}
private void removeMinFreqKey() {
// FK 表中最小freq对应的列表中 最先被插入的那个 key 就是该被淘汰的 key
LinkedHashSet<Integer> keyList = freqToKeys.get(this.minFreq);
int deleteKey = keyList.iterator().next();
keyList.remove(deleteKey);
// 如果 freq 对应的列表空了,就移除这个freq
if(keyList.size() == 0){
freqToKeys.remove(this.minFreq);
// 这里不需要更新 minFreq,因为后面会紧跟一个put操作,minFreq 会被置为1
}
// 更新 KV 表
keyToVal.remove(deleteKey);
// 更新 KF 表
keyToFreq.remove(deleteKey);
}
}