目录
1.设计LRU缓存结构
Get
Set
2.设计LFU缓存结构
Get
Set
Update
这题,说实话,一开始没看懂他的输入到底是什么...
看看说明,好像又是这么一回事,就是创建一个类然后直接调用里面的方法:
其实是维护一个双向链表,写过XV6操作系统的内存和锁那块的同学应该知道这个LRU到底是个啥。思路如下所示:
整个LRU维护一个链表和一个哈希表两个数据结构,链表可以直接用list双向链表,哈希表用unordered_map来设计,哈希表存的是key和在链表中的迭代器的对应关系。
首先,对于get操作,功能是这样的:对于一个给定的key,我们要在链表中查看其是否存在,这里哈希可以实现O(1)的寻找时间代价,处理流程如下:
1.查找是否存在key对应的节点,没有则直接返回-1
2.否则,我们先根据key在哈希中取出对应的节点,然后将对应节点删除。
因为是LRU,所以我们要把最近使用的放在最前面(虽然用了哈希表但是按照LRU的思路还是得放最前面),所以我们把取出来的节点放到链表头部,然后再在哈希表中建立
最后返回头部节点的value
对于set操作,功能是这样的:对于一个给定的
代码如下所示:
class Solution {
public:
list> dlist;
unordered_map>::iterator> map;
int cap;
Solution(int capacity){
cap=capacity;
}
int get(int key) {
// write code here
if(map.count(key)){
auto tmp=*map[key];
dlist.erase(map[key]);
dlist.push_front(tmp);
map[key]=dlist.begin();
return dlist.front().second;
}
return -1;
}
void set(int key, int value){
if(map.count(key)){
dlist.erase(map[key]);
}else if(cap==dlist.size()){
auto tmp=dlist.back();
map.erase(tmp.first);
dlist.pop_back();
}
dlist.push_front(pair(key, value));
map[key]=dlist.begin();
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* solution = new Solution(capacity);
* int output = solution->get(key);
* solution->set(key,value);
*/
当然,除了可以用标准库里的数据结构,我们也可以自己手撸一个双向链表,我们需要分离出以一个将节点移动到表头的函数,以便于当我们在进行get操作找到一个节点,或者set的时候将对应的节点放到表头。
代码如下所示,这里面有很多的细节要注意,主要是数据结构的更新和哈希的更新:
#include
using namespace std;
struct Node{
int key;
int val;
Node *prev;
Node *next;
Node(int key, int val) : key(key), val(val), next(NULL), prev(NULL) {}
};
class Solution {
public:
unordered_map hash;
Node *head;
Node *tail;
int cap;
int size;
Solution(int capacity){
head=NULL;
tail=NULL;
cap=capacity;
size=0;
hash.clear();
}
// 将某个节点放到表头
// 如果位于尾部 需要更新尾节点
// 移到头部后需要更新指针指向以及更新头节点
void removeToHead(Node *p){
// 首先修改
if(p==head){
return;
}
p->prev->next=p->next;
if(p==tail){
tail=p->prev;
}else{
p->next->prev=p->prev;
}
p->prev=NULL;
p->next=head;
head->prev=p;
head=p;
return;
}
int get(int key) {
if(hash.find(key)==hash.end()){
return -1;
}
removeToHead(hash[key]);
return hash[key]->val;
}
void set(int key, int value){
if(hash.find(key)!=hash.end()){
hash[key]->val=value;
removeToHead(hash[key]);
}else if(size>=cap){
//将尾节点更新为当前节点 移动到头部
//更新哈希表 删除原来的 增加新的
int k=tail->key;
hash.erase(k);
tail->key=key;
tail->val=value;
removeToHead(tail);
hash[key]=head;
}else{
//不存在节点 且容量还没满 直接在头部插入
Node *node=new Node(key, value);
if(head==NULL){
head=tail=node;
}else{
node->next=head;
node->prev=NULL;
head->prev=node;
head=node;
}
hash[key]=head;
size++;
}
}
};
上面的LRU是最近最少使用的策略,LFU是least frequently used,从字面上来看貌似也是最近最少使用策略嘛,但是从题目来看,是还让我们记录使用set和get的次数的。
我们这里用一个双哈希表来解决这个题目,同时在题解中看到了一个画的很好的图:
这就是我们要维护的数据结构。首先,对于每个频率,我们维护一个双向链表,这个双向链表代表了对应频率的节点的集合,存储的是{freq, key, value},这样就形成了频率和链表的哈希表。我们根据这个哈希表可以轻松根据频率找到双向链表,在容量不足的时候用来找删除的节点时十分迅速。其次也方便一些删除插入的更新操作。也由于要在容量不足的时候删除节点,我们需要用一个变量来存储freq最小的节点。
第二个哈希表是key和链表节点的哈希表,这个哈希表对于get操作是十分必须的。
接下来解析一下get和set操作的流程:
get操作的功能是根据key找value,有了哈希表之后,我们可以直接根据key找到对应节点,然后找到value,如果没有找到则返回-1。
set操作的功能是设置
1.如果之前存在这个映射关系,那么我们更新一下这个映射,关于更新是什么,之后说。
2.如果不存在,我们就要插入这个新的映射,当然,还要考虑当前的链表是否已经满了,因此我们还需要维护一个记录剩余空间的变量。
a.如果已经满了,我们需要做一些事情:
首先要找到频率最低的且最少使用的节点,对于频率最低的节点,由于我们维护了频率和对应节点的集合的哈希,我们很快可以找到,对于最少使用,我们每次更新的时候是放到表头,所以最少使用的就是最后一个节点。
找到节点之后我们要删除这个节点,删除之后我们还得检查这个链表是否为空,是空的话还得直接删除这个映射项。
b.如果没有满,我们维护一下剩余的容量
之后直接向freq=1的双向链表头部插入新的
更新操作用于在set中已经存在对应的key或者对已存在节点使用了get时,更新其频率。
首先,我们根据key取出链表节点,然后根据链表节点中的freq找到对应的链表,之后直接在链表中删除对应节点。
如果删除后这个链表变成空了:
删除对应映射关系,再看是否需要更新最小频率,如果被删除链表是最小频率对应的链表,那么需要更新
最后将新的
具体代码如下所示:
class Solution {
public:
/**
* lfu design
* @param operators int整型vector> ops
* @param k int整型 the k
* @return int整型vector
*/
// 存频率和频率对应节点链表的字典
unordered_map>> freq_mp;
// 存对应键值和节点
unordered_map>::iterator> mp;
// 记录最小频率 用来使用LFU策略找节点
int min_freq=0;
// 存剩余容量
int size=0;
vector LFU(vector>& operators, int k){
vector res;
size=k;
for(int i=0;i>::iterator iter, int key, int value){
//取出双向链表中的一个节点 即 vector
int freq=(*iter)[0];
freq_mp[freq].erase(iter);
//如果该频率中已经没有节点
//删除频率双向链表中的对应链表
//查看是否需要更新最小频率
if(freq_mp[freq].empty()){
freq_mp.erase(freq);
if(freq == min_freq){
min_freq++;
}
}
//向freq+1的双向链表头部中插入
//由于原来的节点已经不存在 需要重新设置的存储
freq_mp[freq+1].push_front({freq+1, key, value});
mp[key]=freq_mp[freq+1].begin();
}
void set(int key, int value){
auto it=mp.find(key);
if(it!=mp.end()){
//链表中存在节点 更新value和频率
update(it->second, key, value);
}else{
//哈希表中没有该节点
//如果链表已满 删除频率最低而且最早的删掉
//频率表中删除最后一个节点
//删除对应的节点
if(size==0){
int oldkey=freq_mp[min_freq].back()[1];
freq_mp[min_freq].pop_back();
if(freq_mp[min_freq].empty()){
freq_mp.erase(min_freq);
}
mp.erase(oldkey);
}else{
//容量未满 可以直接加入
size--;
}
min_freq=1;
freq_mp[1].push_front({1, key, value});
mp[key]=freq_mp[1].begin();
}
}
//get操作:没有找到则返回-1
//找到则更新频率并且取出value返回
int get(int key){
int res=-1;
auto it = mp.find(key);
if(it==mp.end()){
return res;
}
auto iter = it->second;
res=(*iter)[2];
update(iter, key, res);
return res;
}
};
不得不说,数据结构和相应的存储结合真是一件巧妙的事情~