内存缓存算法-----LRU和LFU

本文详细解析LRU(最近最少使用)和LFU(最不常用)缓存算法的设计与实现,通过双向链表和哈希表结构,确保set和get操作达到O(1)的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LRU:

直接从题目来看:

【题目】
设计一种缓存结构,该结构在构造时确定大小,假设大小为K,并有两个功能:
set(key,value):将记录(key,value)插入该结构。
get(key):返回key对应的value值。

【要求】
1.set和get方法的时间复杂度为O(1)。
2.某个key的set或get操作一旦发生,认为这个key的记录成了最经常使用的。
3.当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的

【举例】
假设缓存结构的实例是cache,大小为3,并依次发生如下行为:
1.cache.set("A",1)。最经常使用的记录为("A",1)。
2.cache.set("B",2)。最经常使用的记录为("B",2),("A",1)变为最不经常的。
3.cache.set("C",3)。最经常使用的记录为("C",2),("A",1)还是最不经常的。
4    cache.get("A")。最经常使用的记录为("A",1),("B",2)变为最不经常的。
5.cache.set("D",4)。大小超过了3,所以移除此时最不经常使用的记录("B",2),
加入记录 ("D",4),并且为最经常使用的记录,然后("C",2)变为最不经常使用的记录

思路:

通过题目我们看到,只要一个记录发生了set或者get操作,说明这个记录的优先级就提到最高,如果我们要删除cache结构中的一个记录,那么优先删除优先级最低的那个记录,即最不常使用的那个记录。

如何实现:

我们用双向链表和map结构来做这个题,map就相当于cache结构,唯一不同的是,map中的value不是题目给出的那个value,而是一个节点类型,而双向链表的节点也是这个节点类型,双向链表的作用是确定优先级的,我们规定双向链表的头部优先级最低,尾部优先级最高,每次从cache结构中删除节点时就删除双向链表的头部的那个节点,我们要自己写这个双向链表,具体操作看代码。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<map>
#include<set>
#define mod 1000000007
typedef long long ll;
using namespace std;
struct Node//这是双向链表的单个节点的结构,也是hash表的value的结构
{
    string key;
    int value;
    Node *last;//这两个Node是为了构建双向链表时用的,last指向当前节点的前一个节点,next指向当前节点的下一个节点
    Node *next;
    Node(string k,int v)//初始化
    {
        key=k;
        value=v;
        last=NULL;
        next=NULL;
    }
};
class MyDqueue//这是双向链表的类
{
private:       //我们规定尾部的优先级最高,头部的优先级最低
    Node *head;//head始终指向双向链表的头
    Node *fail;//fail始终指向双向链表的尾
public:
    MyDqueue()//初始化
    {
        head = NULL;
        fail = NULL;
    }
    void addNode(Node *NewNode)//往双向链表中加入一个节点
    {
        if(NewNode==NULL)//如果要加的节点为空,直接返回
            return ;
        if(head==NULL)//如果这个条件满足,说明双向链表中根本就没有元素,加上这个节点
        {
            head = NewNode;
            fail = NewNode;
        }
        else//否则,双向链表中有节点,将这个节点加入到末尾,使得这个节点的优先级最高,因为是最近调用的,下面是具体的代码实现
        {
            fail->next = NewNode;
            NewNode->last = fail;
            fail = NewNode;
        }
    }
    void MoveNodeFail(Node *MoveNode)//将双向链表中的MoveNode节点移到尾部
    {
        if(MoveNode==NULL)//捣乱的
            return ;
        if(MoveNode==fail)//这个节点本来就在最后,不需要移动了
            return ;
        else if(MoveNode==head)//如果要移动的节点是头节点,需要特殊判断一下,下面的两行代码是将这个节点从双向队列中拿出来的操作
        {
            head = MoveNode->next;
            head->last = NULL;
        }
        else//要移动的节点在双向队列的中间位置,下面的代码是将这个节点从双向队列中拿出来的操作
        {
            MoveNode->next->last = MoveNode->last;
            MoveNode->last->next = MoveNode->next;
        }
        //下面的4行代码是将这个节点移动到双向队列尾部的操作
        MoveNode->last = fail;
        MoveNode->next = NULL;
        fail->next = MoveNode;
        fail = MoveNode;
    }
    Node *MoveHead()//删除双向链表的头部,并且返回头部
    {
        Node *res = new Node(NULL,-1);//函数最后返回这个
        if(head==NULL)//如果双向队列中没有节点,返回空
            return NULL;
        if(head==fail)//如果双向队列中只有一个节点,删除这个节点
        {
            res=head;
            head=NULL;
            fail=NULL;
        }
        else//双向队列中有多个节点,删除头部这个节点
        {
            res = head;
            head = head->next;
            head->last = NULL;
            res->next = NULL;
        }
        return res;//返回头部节点,至于为啥要返回头部节点,是因为这个题目有一个map结构,一个双向链表结构,我们这个函数只删除了双向链表中的这个节点,map中并没有删除,所以要返回这个节点,方便在map中也删除关于这个节点的记录
    }
};
class LRU//这个程序的核心内容
{
private:
    map<string , Node*> Mymap;//定义一个map结构
    MyDqueue Mydqueue;//定义一个我们自己写的双向链表结构
    int Size;//这个Size表示map中最大能够存几条记录
    int NewSize;//这个NewSize表示当前map中已经存了几条记录了
public:
    LRU(int Size,int NewSize)//初始化
    {
        Size = Size;
        NewSize = NewSize;
    }
    int Get(string Key)//得到Key所对应的value,注意,不是map中key所对应的value,因为map中kay所对应的value是一个Node结构体,而是Node结构体里面的value(Node结构体里面的key与map中的那个key是一个东西)
    {
        if(Mymap.find(Key)!=Mymap.end())//这个条件满足,说明有Key这条记录,将这条记录拿出来,并且提高优先级
        {
            Node *res = Mymap[Key];//得到key所对应的Node结构体
            Mydqueue.MoveNodeFail(res);//将这个结构体移动到双向链表的尾部,提高优先级
            return res->value;//返回value至
        }
        return -1;//如果没有key这条记录,返回-1
    }
    void Set(string key,int Value)//这个函数是将Key所对应的value值改成我们传入的vaue值,如果没有key这条记录,就新建一个
    {
        if(Mymap.find(key)!=Mymap.end())//有key这条记录,修改value值,并且提高优先级
        {
            Node *res = Mymap[key];//将key所对应Node结构体拿出来
            res->value = Value;//修改value值
            Mydqueue.MoveNodeFail(res);//提高优先级
        }
        else//如果没有key这条记录,就新建一个,下面是新建key记录的代码
        {
            Node *newNode = new Node(key,Value);
            Mymap[key]=newNode;
            NewSize++;
            Mydqueue.addNode(newNode);
            if(NewSize==Size+1)//如果建完后的记录个数超过我们规定的个数了
            {
               Node *res = Mydqueue.MoveHead();//删除优先级最低的那条记录,也就是双向链表中的头节点所代表的那个记录,这是删除双向链表中的那条记录(其实是删除的节点)
               Mymap.erase(res->key);//这是在map中删除这条记录
            }
        }
    }
};
int main()
{

}

LFU:

题目:

上一题实现了LRU缓存算法,LFU也是一个著名的缓存算法
自行了解之后实现LFU中的set 和 get
要求:两个方法的时间复杂度都为O(1)

思路:

还是cache结构的那两个操作,只不过这次我们用的结构比LRU的那个结构稍微复杂一点,当然,精度也更高了。

具体实现:

我们还是用双向链表和map来做,map还是LRU算法的那种map,只不过双向链表不一样了,我们定义一个双向链表,称为第一级双向链表,然后第一级双向链表的每个节点中再嵌套一个双向链表,成为第二级双向链表,第一级双向链表中的节点代表节点操作的次数,第二级双向链表的节点就和LRU里面的双向链表中节点一样,对于第二级双向链表,我们规定头部的节点的优先级最高,尾部的节点的优先级最低,从cache结构中删数时,就删除第一级链表的头节点中的第二级链表的头节点。具体操作看代码。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<map>
#include<set>
#define mod 1000000007
typedef long long ll;
using namespace std;
struct Node//这是双向链表的单个节点的结构,也是hash表的value的结构
{
    string key;
    int value;
    Node *last;//这两个Node是为了构建双向链表时用的,last指向当前节点的前一个节点,next指向当前节点的下一个节点
    Node *next;
    Node(string k,int v)//初始化
    {
        key=k;
        value=v;
        last=NULL;
        next=NULL;
    }
};
class MyDqueue//这是双向链表的类
{
private:       //我们规定尾部的优先级最高,头部的优先级最低
    Node *head;//head始终指向双向链表的头
    Node *fail;//fail始终指向双向链表的尾
public:
    MyDqueue()//初始化
    {
        head = NULL;
        fail = NULL;
    }
    void addNode(Node *NewNode)//往双向链表中加入一个节点
    {
        if(NewNode==NULL)//如果要加的节点为空,直接返回
            return ;
        if(head==NULL)//如果这个条件满足,说明双向链表中根本就没有元素,加上这个节点
        {
            head = NewNode;
            fail = NewNode;
        }
        else//否则,双向链表中有节点,将这个节点加入到末尾,使得这个节点的优先级最高,因为是最近调用的,下面是具体的代码实现
        {
            fail->next = NewNode;
            NewNode->last = fail;
            fail = NewNode;
        }
    }
    void MoveNodeFail(Node *MoveNode)//将双向链表中的MoveNode节点移到尾部
    {
        if(MoveNode==NULL)//捣乱的
            return ;
        if(MoveNode==fail)//这个节点本来就在最后,不需要移动了
            return ;
        else if(MoveNode==head)//如果要移动的节点是头节点,需要特殊判断一下,下面的两行代码是将这个节点从双向队列中拿出来的操作
        {
            head = MoveNode->next;
            head->last = NULL;
        }
        else//要移动的节点在双向队列的中间位置,下面的代码是将这个节点从双向队列中拿出来的操作
        {
            MoveNode->next->last = MoveNode->last;
            MoveNode->last->next = MoveNode->next;
        }
        //下面的4行代码是将这个节点移动到双向队列尾部的操作
        MoveNode->last = fail;
        MoveNode->next = NULL;
        fail->next = MoveNode;
        fail = MoveNode;
    }
    Node *MoveHead()//删除双向链表的头部,并且返回头部
    {
        Node *res = new Node(NULL,-1);//函数最后返回这个
        if(head==NULL)//如果双向队列中没有节点,返回空
            return NULL;
        if(head==fail)//如果双向队列中只有一个节点,删除这个节点
        {
            res=head;
            head=NULL;
            fail=NULL;
        }
        else//双向队列中有多个节点,删除头部这个节点
        {
            res = head;
            head = head->next;
            head->last = NULL;
            res->next = NULL;
        }
        return res;//返回头部节点,至于为啥要返回头部节点,是因为这个题目有一个map结构,一个双向链表结构,我们这个函数只删除了双向链表中的这个节点,map中并没有删除,所以要返回这个节点,方便在map中也删除关于这个节点的记录
    }
};
class LRU//这个程序的核心内容
{
private:
    map<string , Node*> Mymap;//定义一个map结构
    MyDqueue Mydqueue;//定义一个我们自己写的双向链表结构
    int Size;//这个Size表示map中最大能够存几条记录
    int NewSize;//这个NewSize表示当前map中已经存了几条记录了
public:
    LRU(int Size,int NewSize)//初始化
    {
        Size = Size;
        NewSize = NewSize;
    }
    int Get(string Key)//得到Key所对应的value,注意,不是map中key所对应的value,因为map中kay所对应的value是一个Node结构体,而是Node结构体里面的value(Node结构体里面的key与map中的那个key是一个东西)
    {
        if(Mymap.find(Key)!=Mymap.end())//这个条件满足,说明有Key这条记录,将这条记录拿出来,并且提高优先级
        {
            Node *res = Mymap[Key];//得到key所对应的Node结构体
            Mydqueue.MoveNodeFail(res);//将这个结构体移动到双向链表的尾部,提高优先级
            return res->value;//返回value至
        }
        return -1;//如果没有key这条记录,返回-1
    }
    void Set(string key,int Value)//这个函数是将Key所对应的value值改成我们传入的vaue值,如果没有key这条记录,就新建一个
    {
        if(Mymap.find(key)!=Mymap.end())//有key这条记录,修改value值,并且提高优先级
        {
            Node *res = Mymap[key];//将key所对应Node结构体拿出来
            res->value = Value;//修改value值
            Mydqueue.MoveNodeFail(res);//提高优先级
        }
        else//如果没有key这条记录,就新建一个,下面是新建key记录的代码
        {
            Node *newNode = new Node(key,Value);
            Mymap[key]=newNode;
            NewSize++;
            Mydqueue.addNode(newNode);
            if(NewSize==Size+1)//如果建完后的记录个数超过我们规定的个数了
            {
               Node *res = Mydqueue.MoveHead();//删除优先级最低的那条记录,也就是双向链表中的头节点所代表的那个记录,这是删除双向链表中的那条记录(其实是删除的节点)
               Mymap.erase(res->key);//这是在map中删除这条记录
            }
        }
    }
};
int main()
{

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值