原题链接
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之
前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
示例:
输入:
[“LFUCache”, “put”, “put”, “get”, “put”, “get”, “get”, “put”, “get”, “get”, “g
et”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]
提示:
0 <= capacity, key, value <= 104
最多调用 105 次 get 和 put 方法
进阶:你可以为这两种操作设计时间复杂度为 O(1) 的实现吗?
Related Topics 设计
403 0
实现数据结构: 需要使用 两个哈希表再加上N个双链表才能实现
第一个哈希表中 存储了
HashMapkey-value哈希表
minFreq 是最小访问频次;
HashMap频率哈希表
完整的结构如下图所示:
get操作的具体逻辑大致是这样:
put操作就要复杂多了,大致包括下面几种情况
在代码实现中还需要维护一个minFreq的变量,用来记录LFU缓存中频率最小的元素,在缓存满的时候,可以快速定位到最小频繁的链表,以达到 O(1) 时间复杂度删除一个元素。
具体做法是:
以上参考: 超详细图解+动图演示 460. LFU缓存
代码参考了 算法就像搭乐高:带你手撸 LFU 算法, 其主要思想与前文基本一致, 在实现的数据结构上进行了小改动, 从而极大减少了代码量
LRU 算法的核心数据结构是使用哈希链表 LinkedHashMap
,首先借助链表的有序性使得链表元素维持插入顺序,同时借助哈希映射的快速访问能力使得我们可以在 O(1) 时间访问链表的任意元素。
其在实现时使用了 三个 map, 分别是
HashMap
key 到 val 的映射,我们后文称为 KV 表
HashMap
key 到 freq 的映射,我们后文称为 KF 表
HashMap
freq 到 key 列表的映射,我们后文称为 FK 表
class LFUCache {
HashMap<Integer, Integer> keyToVal; // 存储键值对, key 到 val 的映射,我们后文称为 KV 表
HashMap<Integer, Integer> keyToFreq; // 存储key和当前key的使用频率, key 到 freq 的映射,我们后文称为 KF 表
HashMap<Integer, LinkedHashSet<Integer>> freqToKeys; // 一个频率可能对应多个key,存储频率与key的一对多映射, FK 表
int cap; // LFU缓存最大容量
int minFreq; // 记录当前最小使用频率
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)) {
// 先更新当前key的使用频率再返回val
increaseFreq(key);
return keyToVal.get(key);
} else return -1; // 不存在当前key
}
public void put(int key, int value) {
if (this.cap <= 0) return; // 如果当前最大容量小于等于0,则直接返回null
// put时分两种情况:1. key存在,2.key不存在
if (keyToVal.containsKey(key)) { // 1. key存在
keyToVal.put(key, value);
increaseFreq(key); // 更新key对应的频率+1
return;
}
// 2.key不存在
if (keyToVal.size() >= this.cap) { // 当前缓存已满
removeMinFreqKey(); // 删除使用频率最小,有多个时删除数据最老的
}
keyToVal.put(key, value); //插入FV表
keyToFreq.put(key, 1); // 插入FK表
freqToKeys.putIfAbsent(1, new LinkedHashSet<Integer>()); // 确保1对应的链表存在
freqToKeys.get(1).add(key); // 加入频率为1对应的链表中
// 由于是新加入的, 因此minFreq 更新为1(最小就为1)
this.minFreq = 1;
}
public void increaseFreq(int key) {
int freq = keyToFreq.get(key);
keyToFreq.put(key, freq + 1); // 更新kF表
// 更新FK表
freqToKeys.get(freq).remove(key);
freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<Integer>());
freqToKeys.get(freq + 1).add(key);
// 判断freq对应的链表是否没有元素了
if (freqToKeys.get(freq).isEmpty()) {
freqToKeys.remove(freq);
if (freq == this.minFreq) { // 如果freq正好是minFreq, 则更新minFreq
this.minFreq++;
}
}
}
public void removeMinFreqKey() {
// 先获取到minFreq对应的链表
LinkedHashSet<Integer> keyList = freqToKeys.get(this.minFreq);
// 由于链表是有序的, 因此第一个加入的一定是频率最小且存在时间最久的那个
int deleteKey = keyList.iterator().next();
keyList.remove(deleteKey); // 更新Fk表
if (keyList.isEmpty()) { // 如果当前minFreq对应的链表已经没有元素了
freqToKeys.remove(this.minFreq); // 移除当前minFreq对应的链表
// 注:在此不用再更新minFreq, 因为每次put时更新了minFreq为1
}
keyToVal.remove(deleteKey); // 更新 kV 表
keyToFreq.remove(deleteKey); // 更新 kF 表
}
}
详细代码说明可以参考链接: 算法就像搭乐高:带你手撸 LFU 算法