LFU 缓存淘汰算法
Least Frequently Used(LFU): 淘汰使用次数最少的数据。
LFU 算法的需求
- 接收一个参数
capacity
作为缓存的最大容量 - 实现一个函数
put()
添加数据到缓存 - 实现一个函数
get()
查询缓存中的数据 - 以上函数应该在 \(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);
}
}