手撕LFU缓存

news2025/1/18 17:15:48

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/884652.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

16.3.4 【Linux】系统资源的观察

free &#xff1a;观察内存使用情况 系统当中有 2848MB 左右的实体内存&#xff0c;我的 swap 有 1GB 左右&#xff0c; 那我使用free -m 以 MBytes 来显示时&#xff0c;就会出现上面的信息。Mem 那一行显示的是实体内存的量&#xff0c;Swap 则是内存交换空间的量。 total 是…

Espresso测试用例运行前置配置条件

&#xff08;一&#xff09;【开发者选择】将USB调试/USB安装/USB调试&#xff08;安全设置&#xff09;开启&#xff1b; &#xff08;否则运行测试用例时&#xff0c;无法安装&#xff09; &#xff08;二&#xff09;开启【自启动】 &#xff08;需要授权&#xff0c;否则无…

51单片机的管脚介绍

图文介绍 纯文字说明 单片机管脚相关结构及其作用如下 电源正极引脚 一般接5V电源&#xff0c;为单片机提供正常工作时的电压。 电源负极引脚 接地。然后才开始工作。 时钟引脚 18、19脚为时钟引脚&#xff08;XTAL2、XTAL1&#xff09;。单片机内部有大量的数字电路&a…

Docker部署ES服务,canal全量同步的时候内存爆炸,ES/Canal Adapter自动关闭,CPU100%

文章目录 问题解决方案1. 对ES的限制2. 对Canal-Adapter的限制 问题 使用canal-adapter全量同步&#xff08;参考Canal Adapter1.1.5版本API操作服务&#xff0c;手动同步数据&#xff08;4&#xff09;&#xff09;的时候 小批量数据可以正常运行&#xff08;几千条&#xf…

Python获取、修改主机名称和IP地址实践

Python获取、修改主机名称和IP地址的方法有多种&#xff0c;内置socket模块、执行系统命令、第三方模块等等&#xff0c;本文只是完成功能的一次成功的实践。 1. 获取、修改主机名称 本案例使用python的socket模块获取、修改主机名称&#xff0c;socket模块是一个用于实现网络…

对抗通胀的信心回升,美国消费者预期短期通胀降至两年多低点

KlipC报道&#xff1a;据纽约联储最新消费者预期调查显示美国消费者预计一年后短期通胀从3.8%下降至3.5%&#xff0c;创2021年4月以来新低&#xff0c;为连续第四个月下降。三年期和五年期的通胀预期也下降&#xff0c;未来几年通胀将会走弱。 与此同时他们对个人的财务状况也趋…

JDK8 Optional优雅的判空操作

一、前言 在我们编程过程中因为疏忽经常会遇到NullPointerException空指针异常&#xff0c;所以与此同时就避免不了对变量一层一层的进行判空。JDK8新特性所提供的Optional类&#xff0c;可以让我们更优雅的进行判空操作。学习其的意义就是提醒开发者注意空值情况&#xff0c;…

怎么自己制作动图表情包?在线gif生成的操作步骤

gif表情包在我们平时的生活里斗图的时候经常会用到&#xff0c;那么如何用图片制作gif&#xff08;https://www.gif.cn&#xff09;表情包呢&#xff1f;今天就分享一个在线gif生成的简单方法&#xff0c;利用gif制作工具将图片转gif动图&#xff0c;下面是详细的操作步骤。 打…

爬楼梯(一次爬1或2层)

一&#xff0c;题目描述 二&#xff0c;解题思路 动态规划 动规五部曲&#xff1a; 1. 确认dp数组以及下标含义 2. 推导递推公式 3. 确认dp数组如何初始化 4. 确认遍历顺序 5. 打印dp数组 dp数组含义&#xff1a;到第i层的方法数目 下标含义&#xff1a;层数 递推公式&…

2023年服贸会在哪里举行?北京有哪些媒体可以做宣传?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 8月14日&#xff0c;”2023年服贸会新闻发布会在北京首钢园举行&#xff0c;宣布2023年服贸会将于9月2日至6日在国家会议中心和首钢园区举办&#xff0c;采用‘线下线上’‘综合专题’办…

跳跳!(贪心)

题目描述 你是一只小跳蛙&#xff0c;你特别擅长在各种地方跳来跳去。 这一天&#xff0c;你和朋友小 F 一起出去玩耍的时候&#xff0c;遇到了一堆高矮不同的石头&#xff0c;其中第 i 块的石头高度为 h_i&#xff0c;地面的高度是 h_0 0。你估计着&#xff0c;从第 i 块石…

eNSP:mplsvpn综合实验

要求完成下图实验&#xff1a; 拓扑信息标注&#xff1a; 命令操作&#xff1a; r1: <Huawei>sys [Huawei]sys r1 [r1]int lo0 [r1-LoopBack0]ip add 192.168.1.1 24 [r1-LoopBack0]int g 0/0/0 [r1-GigabitEthernet0/0/0]ip add 192.168.2.1 30[r1]ip route-static 19…

【BASH】回顾与知识点梳理(二十九)

【BASH】回顾与知识点梳理 二十九 二十九. 进程和工作管理29.1 什么是进程 (process)进程与程序 (process & program)子进程与父进程&#xff1a;fork and exec&#xff1a;进程呼叫的流程系统或网络服务&#xff1a;常驻在内存的进程 29.2 Linux 的多人多任务环境多人环境…

C#__委托的基本用法

// 委托A class DelegateA{// 委托&#xff0c;一个类型&#xff0c;可以赋值一个方法的引用// 定义delegate void Tool(int x);delegate long ToolLong(long x, long y);delegate string GetString();public void Delegate(){// 赋值Tool tool null;ToolLong toollong null…

clickhouse 集群搭建

目录 一、 首先要安装单节点集群 二、安装zookeeper并搭建集群 1. 配置host 2. 安装zookeeper 3. 配置zookeeper 三、clickhouse 集群配置 1.配置metrika.xml文件 2. 修改clickhouse的配置文件 一、 首先要安装单节点集群 在这里就不太赘述。官网下载安装包&#xff0…

离线安装Nginx(rpm方式)

环境&#xff1a;centos7.9 下面将展示通过rpm方式安装Nginx 1.官方下载Nginx rpm包 下载地址 http://nginx.org/packages/ 下载版本根据操作系统版本进行选择&#xff0c;本次操作系统是centos7.9 - 64位系统&#xff0c;所以对应的下载路径是&#xff1a; http://nginx.or…

视频监控管理平台EasyCVR视频平台通过navicat修改登录密码的具体操作步骤

TSINGSEE青犀视频监控管理平台EasyCVR可以根据不同的应用场景需求&#xff0c;让平台在内网、专网、VPN、广域网、互联网等各种环境下进行音视频的采集、接入与多端分发。在视频能力上&#xff0c;平台可实现视频实时直播、云端录像、云存储、回放与检索、告警上报、视频快照、…

OJ练习第147题——字符串中的查找与替换

字符串中的查找与替换 力扣链接&#xff1a;833. 字符串中的查找与替换 题目描述 你会得到一个字符串 s (索引从 0 开始)&#xff0c;你必须对它执行 k 个替换操作。替换操作以三个长度均为 k 的并行数组给出&#xff1a;indices, sources, targets。 要完成第 i 个替换操…

RDMA Send Receive操作

1. 前言 RDMA指的是远程直接内存访问&#xff0c;这是一种通过网络在两个应用程序之间搬运缓冲区里的数据的方法。RDMA与传统的网络接口不同&#xff0c;因为它绕过了操作系统。这允许实现了RDMA的程序具有如下特点&#xff1a; 绝对的最低时延最高的吞吐量最小的CPU足迹 &am…

《游戏编程模式》学习笔记(四) 观察者模式 Observer Pattern

定义 观察者模式定义了对象间的一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。 这是定义&#xff0c;看不懂就看不懂吧&#xff0c;我接下来举个例子慢慢说 为什么我们需要观察者模式 我们看一个很简…