手撕LFU缓存

本文介绍了如何实现LFU(LeastFrequentlyUsed)缓存,它是LRU的扩展,考虑了访问次数。作者通过C++代码展示了LFU的结构,包括使用两个哈希表和多个双链表来存储不同频率的键值对,以满足O(1)的平均时间复杂度要求。

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

手撕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);
       }    
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BearPot

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值