刚学完LRU就来LFU。。。
设计并实现最不经常使用(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
LFU(Least Frequently Used)最不经常使用,如果一个数据在最近一段时间内使用次数很好,那么在将来一段时间内被使用的可能性也很小。
LRU与LFU的区别:
LRU优先淘汰最长时间未被使用的页面,LFU优先淘汰一定时期内被访问次数最少的页面。
在C++中可以直接使用std::set类作为平衡二叉树;在Java语言中可以直接使用TreeSet。Python中没有内置的库来实现模拟平衡二叉树。使用哈希表以键key为索引存储缓存。
常用的平衡二叉树(Balanced Binary Tree)有:AVL树,红黑树(RB Tree),伸展树(Splay Tree)。
/*
哈希表+平衡二叉树
使用平衡二叉树的性质维护了时间的顺序
*/
// 缓存的数据结构
struct Node {
int cnt; // 使用频率
int time; // 最近一次使用时间
int key, value; // kv键值对
// 我们需要实现一个Node类的比较函数,将cnt作为第一关键字,time作为第二关键字
bool operator < (const Node& rhs) const {
return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;
}
// Node构造函数
Node (int _cnt, int _time, int _key, int _value): cnt(_cnt), time(_time), key(_key), value(_value){}
};
class LFUCache {
// 缓存容量,时间戳;
int capacity, time;
unordered_map<int, Node> key_table; // 哈希表
set<Node> S; // 平衡二叉树
public:
LFUCache(int _capacity) {
this->capacity = _capacity;
this->time = 0;
this->key_table.clear();
this->S.clear();
}
int get(int key) {
if (this->capacity == 0) return -1;
auto it = key_table.find(key);
// 如果哈希表中没有要查找的键值key,返回-1;
if (it == key_table.end()) return -1;
// 从哈希表中得到旧的缓存
Node cache = it->second;
S.erase(cache);
// 将旧缓存更新
cache.cnt += 1;
cache.time = ++time;
// 将缓存重新放入哈希表和平衡二叉树中
S.insert(cache);
it->second = cache;
return cache.value;
}
void put(int key, int value) {
if (this->capacity == 0) return;
auto it = key_table.find(key);
// 如果没有键值key
if (it == key_table.end()) {
// 到达缓存容量上限
if (key_table.size() == this->capacity) {
// 从哈希表和平衡二叉树中删除最近最少使用的缓存
key_table.erase(S.begin()->key);
S.erase(S.begin());
}
// 创建新的缓存
Node cache = Node(1, ++time, key, value);
// 将新缓存放入哈希表和平衡二叉树中
key_table.insert(make_pair(key, cache));
S.insert(cache);
}
else {
// 如果存在键值key,则和get函数类似,只是需要改变value值
Node cache = it->second;
S.erase(cache);
cache.cnt += 1;
cache.time = ++time;
cache.value = value; // 改变value的值
S.insert(cache);
it->second = cache;
}
}
};
定义两个哈希表,第一个哈希表freq_table以频率freq为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为freq的缓存,缓存里存放三个信息,分别为键key,值value,以及使用频率freq。第二个key_table以键值key为索引,每个索引存放对应缓存在freq_table中链表里的内存地址。利用两个哈希表来使得两个操作的时间复杂度均为O(1)。同时需要记录一个当前缓存最少使用的频率minFreq,这是为了删除操作服务的。
对于get(key)操作,我们能通过索引key在key_table中找到缓存在freq_table中的链表的内存地址,如果不存在直接返回-1,否则我们能获取到对应缓存的相关信息,这样我们就能知道缓存的键值还有使用频率,直接返回key对应的值即可。在get操作之后,这个缓存的使用频率加一,所以需要更新缓存在哈希表freq_table中的位置。已知这个缓存的键key,值value,以及使用频率freq,则该缓存应该存放到freq_table中freq+1索引下的链表中。所以需要在当前链表中以O(1)的时间复杂度删除该缓存对应的节点,根据情况更新minFreq值,然后将其以O(1)的时间复杂度插入到freq + 1索引下的链表头完成更新。这其中的操作复杂度均为O(1)。插入到链表头是为了保证缓存在当前链表中从链表头到链表尾的插入时间是有序的,为后面的删除操作服务。
对于put(key, value)操作,先通过索引key在key_table中查看是否有对应的缓存,如果有的话,其操作等价于get(key)操作,唯一的区别就是要更新value。如果没有对应的key就需要插入新的缓存,如果容量已满就需要删除最近最不经常使用的缓存,再进行插入。对于新插入的缓存,使用频率一定是1,所以将缓存的信息插入到freq_table中1索引下的列表头即可,同时更新key_table[key]的信息,以及更新minFreq = 1。对于删除操作,可以用过minFreq知道freq_table里目前最少使用频率的索引,同时因为我们保证了链表中从链表头到链表尾的插入时间是有序的,所以freq_table[minFreq]的链表中链表尾的节点即为使用频率最小且插入时间最早的节点,我们删除它同时根据情况更新minFreq,整个时间复杂度均为O(1)。
struct Node {
int key, val, freq;
Node (int _key, int _val, int _freq): key(_key), val(_val), freq(_freq) {}
};
class LFUCache {
int minfreq, capacity;
unordered_map<int, list<Node>::iterator> keyTable;
unordered_map<int, list<Node> > freqTable;
public:
LFUCache(int _capacity) {
this->minfreq = 0;
this->capacity = _capacity;
this->freqTable.clear();
this->keyTable.clear();
}
int get(int key) {
if (this->capacity == 0) return -1;
auto it = this->keyTable.find(key);
// 不存在该键值key
if (it == this->keyTable.end()) return -1;
// 存在该键值,访问该键值
list<Node>::iterator node = it->second;
// 记录值val和freq值
int val = node->val, freq = node->freq;
freqTable[freq].erase(node); // 从旧的频率链表里删除
// 如果当前链表为空,需要在哈希表中删除,并且更新minFreq
if (freqTable[freq].size() == 0) {
freqTable.erase(freq);
if (minfreq == freq) {
minfreq += 1;
}
}
// 插入到freq+1链表的表头
freqTable[freq+1].push_front(Node(key, val, freq+1));
keyTable[key] = freqTable[freq+1].begin();
return val;
}
void put(int key, int value) {
if (this->capacity == 0) return;
auto it = keyTable.find(key);
// 缓存中不存在key,直接进行插入即可
if (it == keyTable.end()) {
// 缓存已满,需要进行删除操作
if (keyTable.size() == capacity) {
// 通过minFreq拿到freqTable[minFreq]链表的末尾节点(最久未被使用的低频率节点)
auto it2 = freqTable[minfreq].back();
keyTable.erase(it2.key);
freqTable[minfreq].pop_back();
if (freqTable[minfreq].size() == 0) {
freqTable.erase(minfreq);
}
}
freqTable[1].push_front(Node(key, value, 1));
keyTable[key] = freqTable[1].begin();
minfreq = 1;
}
else {
// 与get操作基本一致,除了需要更新缓存的值
list<Node>::iterator node = it->second;
int freq = node->freq;
freqTable[freq].erase(node);
if (freqTable[freq].size() == 0) {
freqTable.erase(freq);
if (minfreq == freq) {
minfreq += 1;
}
}
freqTable[freq+1].push_front(Node(key, value, freq+1));
keyTable[key] = freqTable[freq+1].begin();
}
}
};