LeetCode 每日一题 460. LFU缓存
2020.04.05
难度 困难
题目
设计并实现最不经常使用(LFU)缓存的数据结构。它应该支持以下操作:get 和 put。
get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
put(key, value) - 如果键不存在,请设置或插入值。当缓存达到其容量时,它应该在插入新项目之前,使最不经常使用的项目无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,最近最少使用的键将被去除。
进阶:
你是否可以在 O(1) 时间复杂度内执行两项操作?
示例:
LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 去除 key 2
cache.get(2); // 返回 -1 (未找到key 2)
cache.get(3); // 返回 3
cache.put(4, 4); // 去除 key 1
cache.get(1); // 返回 -1 (未找到 key 1)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
题解一:平衡二叉树+哈希表
这题我们可以使用平衡二叉树和哈希表的组合来解决这个问题。我们这的平衡二叉树在C++中STL中的<set>
代替,<set>
的底层实现也是平衡二叉树。哈希表的话我们这里使用的unordered_map,这里使用unordered_map而不是map的原因是,unordered_map在内部实现了哈希表,查询速度非常快,但是建表的速度稍慢,适合查询较多的数据。
下面我们来说具体的实现,首先我们这里需要实现的有LFUCache()的构造函数、get()方法、put()方法。我们这里首先需要一个结构体进行存储我们的各个结点的数据(使用频率、缓存的使用时间、键key、值value),这里我们使用一个Node结构体。在结构体内我们写了一个赋值函数Node(int _cnt, int _time, int _key, int _value)
,方便我们进行赋值,然后由于我们需要将Node存储进<set>
中,所以我们需要重载<
运算符,这里的<
符号的运算规则采用最近最少使用原则
进行运算,在代码中进行了更详细的规则解释。
我们在LFUCache类中需要变量capacity 表示缓存容量,time 表示当前时间,unordered_map<int, Node> hash_map;
hash_map用于存储key和Node,set<Node> S;
用于存储结点,并且自动的从小到大排序,这就是我们类需要使用的四个变量。在构造函数LFUCache(int _capacity)
中,我们只需要执行初始化操作即可。
get(int key)
方法的实现,我们只需要在hash_map中进行查找,如果查找到了,则返回值,并更新数据,没查找到则返回-1
put(int key, int value)
方法的实现,就是在hash_map中对key进行查找,如果查找到了,则更新数据(和get()中类似),如果没查到到,则需要判断容量是否满,如果满,则删除最近最少使用的结点即S的开始结点
,然后再进行插入操作,如果容量没满,则直接进行插入操作。
下面贴上详细注释的解题代码,供大家参考:
struct Node {
// cnt 表示缓存使用的频率,time 表示缓存的使用时间,key 表示缓存键, value 表示缓存的值
int cnt, time, key, value;
// 赋值函数
Node(int _cnt, int _time, int _key, int _value){
cnt = _cnt;
time = _time;
key = _key;
value = _value;
}
// 重载运算符,因为这里使用set,需要对`<`运算符进行重载
bool operator < (const Node& mid) const {
// 返回的值为:如果比较的Node和当前的Node的使用频率一样,则返回比较两者的使用时间,如果当前时间小则返回1(当前的最近使用,最近最少使用原则),否则返回0;如果使用频率不一样,则返回两者时间的比值,即以cnt的大小作为判断。
return cnt == mid.cnt ? time < mid.time : cnt < mid.cnt;
}
};
class LFUCache {
// capacity 表示缓存容量,time 表示当前时间
int capacity, time;
// 这里使用了unordered_map而不是map,因为unordered_map内部实现了哈希表,查找速度快,适用于查找多的结构
// hash_map用于存储key和Node
unordered_map<int, Node> hash_map;
// S用于存储Node,并且自动的排序,从小到大
set<Node> S;
public:
LFUCache(int _capacity) {
// 第一次调用时,进行初始化
capacity = _capacity;
time = 0;
hash_map.clear();
S.clear();
}
int get(int key) {
// 如果容量为空,则一直无法存储,直接返回-1
if (capacity == 0)
return -1;
// 在hash_map中查找key
auto it = hash_map.find(key);
// 如果哈希表中没有键 key,返回 -1
if (it == hash_map.end())
return -1;
// 如果查找到了key,则从哈希表中得到旧的缓存,这里的second即为unordered_map<int, Node> hash_map;中的第二个参数,Node类型,
Node cache = it -> second;
// 从平衡二叉树中删除旧的缓存
S.erase(cache);
// 将旧缓存更新,使用频次+1,时间为当前时间+1
cache.cnt += 1;
cache.time = ++time;
// 将新缓存重新放入哈希表和平衡二叉树中
S.insert(cache);
it -> second = cache;
return cache.value;
}
void put(int key, int value) {
// 如果容量为0,则直接返回,即不操作
if (capacity == 0)
return;
// 在hash_map中查找Key
auto it = hash_map.find(key);
// 如果没查找到key,则进行插入
if (it == hash_map.end()) {
// 如果到达缓存容量上限
if (hash_map.size() == capacity) {
// 分别从哈希表和平衡二叉树中删除最近最少使用的缓存,S是按照最近最少使用进行排序的,所以第一个也就是最近最少使用的那个。
hash_map.erase(S.begin() -> key);
S.erase(S.begin());
}
// 创建新的缓存,使用频次初始为1,时间为当前时间+1,key,value即参数中的键值对
Node cache = Node(1, ++time, key, value);
// 将新缓存放入哈希表和平衡二叉树中
hash_map.insert(make_pair(key, cache));
S.insert(cache);
}
else {
// 如果找到了key,则进行一个更新操作,和get()的更新操作类似
Node cache = it -> second;
S.erase(cache);
cache.cnt += 1;
cache.time = ++time;
cache.value = value;
S.insert(cache);
it -> second = cache;
}
}
};
/**
* Your LFUCache object will be instantiated and called as such:
* LFUCache* obj = new LFUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
执行结果为: