手撕LRU缓存_右大臣的博客-CSDN博客
是LRU的升级,多了一个访问次数的维度
实现 LFUCache
类:
LFUCache(int capacity)
- 用数据结构的容量capacity
初始化对象int get(int key)
- 如果键key
存在于缓存中,则获取键的值,否则返回-1
。void put(int key, int value)
- 如果键key
已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量capacity
时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1
(由于 put 操作)。对缓存中的键执行 get
或 put
操作,使用计数器的值将会递增。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
LRU的实现是一个哈希表加上一个双链表
而LFU则要复杂多了,需要用两个哈希表再加上N个双链表才能实现
LFU需要为每一个频率构造一个双向链表
460. LFU 缓存 - 力扣(LeetCode) 一个逻辑讲解
总的来说就是下图这样:
所以针对LRU的模仿写法,我就直接用STL的list去做
因为LFU多了频率,所以需要重构结点,,就不像LRU那个一样用pair对组了
//因为多了频率,所以重构结点,,就不像LRU那个一样用pair对组了
struct Node
{
int key;
int value;
int frequency;
Node(int k,int v,int f=1):key(k),value(v),frequency(f){}
};
class LFUCache {
private:
int cap;
int minfre;//最小的频率
unordered_map<int,list<Node>::iterator>mpkey;//用记录key -val 缓存
unordered_map<int,list<Node>>mpfre;//记录频率访问次数的
public:
LFUCache(int capacity=10) {
cap = capacity;
minfre = 1;
mpkey.clear();
mpfre.clear();
}
int get(int key) {
//没有这个结点
if(mpkey.count(key)==0){
return -1;
}
//有结点 ,保存结点信息
auto it=mpkey[key];//找到结点位置
int fre1=it->frequency;//找到对应的频率
int val=it->value;
//开始操作记录频率的表了
mpfre[fre1].erase(it);//删除这个频率链表上的结点
if(mpfre[fre1].size()==0){//删完如果他空了
//就把这个链表给删了
mpfre.erase(fre1);
if(fre1==minfre){
//如果这个频率他刚好是最小的
minfre++;
}
}
//把这个结点加到高频率的链表头
mpfre[fre1+1].push_front(Node(key,val,fre1+1));
//更新结点缓存表的指向
mpkey[key]=mpfre[fre1+1].begin();
return mpkey[key]->value;
}
void put(int key, int value) {
if(get(key)!=-1){
//有这个结点
//get已经帮忙更新过了,只需要把值改了就行
mpkey[key]->value=value;
}
else{
//没有这个结点,需要新添加
//看结点缓存满不满
if(mpkey.size()==cap){
//根据LRU算法,找到最小频率的尾巴的结点,删除
auto it=mpfre[minfre].back();
mpkey.erase(it.key);
mpfre[minfre].pop_back();
//如果删完 最小频率这个链表他空了
if(mpfre[minfre].size()==0){
mpfre.erase(minfre);
}
}
//添加 新添加的频率置为1
int freque=1;
mpfre[freque].push_front(Node(key,value,freque));
mpkey[key]=mpfre[freque].begin();
//最小频率置为1
minfre=1;
}
}
};
下面是我们自己实现的双向循环链表的写法,代替list
构建结点和链表
为了方便操作,给双向链表加上了虚拟的头结点和尾结点,并在初始化的时候首尾相接。
struct Node
{
int key;
int value;
int frequency;
Node*prev;
Node*next;
Node():key(-1),value(-1),frequency(0),prev(nullptr),next(nullptr){}
Node(int k,int v):key(k),value(v),frequency(1),prev(nullptr),next(nullptr){}
};
struct List//双向链表
{
int frequency;
//虚拟 头 尾 结点
Node*vhead;
Node*vtail;
List(int f):frequency(f),vhead(new Node()),vtail(new Node()){
vhead->next=vtail;
vtail->prev=vhead;
}
};
有了基本结构就能实现LFU以及自己手撕链表的一些简单操作
这里需要用到的有,插入,删除结点,以及链表判空,和返回链表最后一个尾结点
struct Node
{
int key;
int value;
int frequency;
Node*prev;
Node*next;
Node():key(-1),value(-1),frequency(0),prev(nullptr),next(nullptr){}
Node(int k,int v):key(k),value(v),frequency(1),prev(nullptr),next(nullptr){}
};
struct List//双向链表
{
int frequency;
//虚拟 头 尾 结点
Node*vhead;
Node*vtail;
List(int f):frequency(f),vhead(new Node()),vtail(new Node()){
vhead->next=vtail;
vtail->prev=vhead;
}
};
class LFUCache {
private:
int cap;
int minfre;
unordered_map<int,Node*>keymp;
unordered_map<int,List*>fremp;
public:
LFUCache(int capacity=10):cap(capacity),minfre(1) {
}
bool empty(List *ls)
{
return ls->vhead->next==ls->vtail?true:false;
}
void destroy(Node*node)
{
node->prev->next=node->next;
node->next->prev=node->prev;
}
void addNode(Node*node)
{
int fre=node->frequency;
if(!fremp.count(fre)){ //如果结点不在
fremp[fre]=new List(fre); //创建一个新链表
}
List*ls=fremp[fre];
//开始插入结点
node->next=ls->vhead->next;
ls->vhead->next->prev=node;
node->prev=ls->vhead;
ls->vhead->next=node;
}
void popTail()
{
Node*tmp=fremp[minfre]->vtail->prev;
destroy(tmp);
keymp.erase(tmp->key);
}
int get(int key) {
if(keymp.count(key))
{
//存在
Node*cur=keymp[key];
int val=cur->value;
int fre=cur->frequency;
destroy(cur);
//删完之后如果 链表空了,那就删链表
if(empty(fremp[fre])){
fremp.erase(fre);
if(fre==minfre){
minfre++;
}
}
//加结点
cur->frequency+=1;
addNode(cur);
return val;
}
return -1;
}
void put(int key, int value) {
if(get(key)!=-1){
keymp[key]->value=value;
}
else{
//满了没
if(keymp.size()==cap){
popTail();
}
Node*cur=new Node(key,value);
keymp[key]=cur;
minfre=1;
addNode(cur);
}
}
};