LRU-LFU缓存算法

文章目录

    • 缓存算法
      • LRU缓存算法
      • LFU缓存算法
        • 定义
        • 实现
          • 方法一:哈希表+平衡二叉树
          • 方法二:双哈希表+哈希链表
          • 方法三:双哈希表

缓存算法

LRU缓存算法

https://labuladong.online/algo/data-structure/lru-cache/

LRU(Least Recently Used)

LFU缓存算法

定义

LFU:Least Frequently Used,也就是每次淘汰那些使用次数最少的数据。

实现
方法一:哈希表+平衡二叉树

比较直观的想法就是我们用哈希表 key_table 以键 key 为索引存储缓存,建立一个平衡二叉树 S 来保持缓存根据 (cnt,time) 双关键字由于。在 C++ 中,我们可以使用 STL 提供的 std::set 类,set 背后的实现是红黑树:

对于 get(key) 操作,我们只要查看一下哈希表 key_table 是否有 key 这个键即可,有的话需要同时更新哈希表和集合中该缓存的使用频率以及使用时间,否则返回 -1。

对于 put(key, value) 操作,首先需要查看 key_table 中是否已有对应的键值。如果有的话操作基本等同于 get(key),不同的是需要更新缓存的 value 值。如果没有的话相当于是新插入一个缓存,这时候需要先查看是否达到缓存容量 capacity,如果达到了的话,需要删除最近最少使用的缓存,即平衡二叉树中最左边的结点,同时删除 key_table 中对应的索引,最后向 key_table 和 S 插入新的缓存信息即可。


/*
https://leetcode.cn/problems/lfu-cache/description/
*/

struct Node {
    int timeStamp;
    int freq;
    int key;
    int val;

    Node (int key, int val, int freq, int timeStamp) : key(key), val(val), freq(freq), timeStamp(timeStamp) {}
    
    bool operator< (const Node& rhs) const
    {
        // 第一优先级,最久未使用的:频率小的
        if (freq != rhs.freq) {
            return freq < rhs.freq;
        }

        // 第二优先级,最久未使用的:时间老的
       
        return timeStamp < rhs.timeStamp;
    }
};

// leetCode 
class LFUCache {
public:
    LFUCache(int capacity) {
        capacity_ = capacity;
        curTime_ = 0;
        keyToCache_.clear();
    }
    
    int get(int key) {
        if (capacity_ == 0) {
            return -1;
        }

        // 找不到则返回-1
        auto iter = keyToCache_.find(key); 
        if (iter == keyToCache_.end()) {
            return -1;
        }

        // 找到了需要更新各属性
        Node& cache = iter->second;
        // 从平衡二叉树中删除旧的缓存
        cacheSet_.erase(cache);

        cache.timeStamp = ++curTime_;
        cache.freq++;

        // 将新缓存重新放入哈希表和平衡二叉树中
        cacheSet_.insert(cache);
        iter->second = cache;
        return cache.val;
    }
    
    void put(int key, int value) {
        if (capacity_ == 0) {
            return;
        }

        auto it = keyToCache_.find(key);
        // 缓存中没有
        if (it == keyToCache_.end()) {
            // 如果到达缓存容量上限
            if (keyToCache_.size() == capacity_) {
                // 从哈希表和平衡二叉树中删除最近最少使用的缓存
                keyToCache_.erase(cacheSet_.begin()->key); // 在set中存key的原因
                cacheSet_.erase(cacheSet_.begin());
            }
            // 创建新的缓存
            Node cache = Node(key, value, 1, ++curTime_);
            // 将新缓存放入哈希表和平衡二叉树中
            keyToCache_.insert(make_pair(key, cache));
            cacheSet_.insert(cache);
        } else { // 缓存中已经有了,则更新各个属性
            Node cache = it->second;
            // 从平衡二叉树中删除旧的缓存
            cacheSet_.erase(cache);

            cache.timeStamp = ++curTime_;
            cache.freq++;
            cache.val = value;

            // 将新缓存重新放入哈希表和平衡二叉树中
            cacheSet_.insert(cache);
            it->second = cache;
        }
    }

private:
    int capacity_;
    int curTime_;
    unordered_map<int, Node> keyToCache_; // 指向cache的key-val
    set<Node> cacheSet_; // cache 队列
};

在这里插入图片描述

方法二:双哈希表+哈希链表

参考:https://blog.csdn.net/sj15814963053/article/details/122731147
一定先从最简单的开始,根据 LFU 算法的逻辑,我们先列举出算法执行过程中的几个显而易见的事实:

1、调用 get(key) 方法时,要返回该 key 对应的 val。

2、只要用 get 或者 put 方法访问一次某个 key,该 key 的 freq 就要加一。

3、如果在容量满了的时候进行插入,则需要将 freq 最小的 key 删除,如果最小的 freq 对应多个 key,则删除其中最旧的那一个。

好的,我们希望能够在 O(1) 的时间内解决这些需求,可以使用基本数据结构来逐个击破:

1、使用一个 HashMap 存储 key 到 val 的映射,就可以快速计算 get(key)。

unordered_map<int, int> keyToVal;

2、使用一个 HashMap 存储 key 到 freq 的映射,就可以快速操作 key 对应的 freq。

unordered_map<int, int> keyToFreq;

3、这个需求应该是 LFU 算法的核心,所以我们分开说。

  • a.首先,肯定是需要freq到key的映射,用来找到freq最小的key。

  • b.将freq最小的key删除,那你就得快速得到当前所有key最小的freq是多少。想要时间复杂度 O(1) 的话,肯定不能遍历一遍去找,那就用一个变量minFreq来记录当前最小的freq吧。

  • c.可能有多个key拥有相同的freq,所以 freq对key是一对多的关系,即一个freq对应一个key的列表。

  • d.希望freq对应的key的列表是存在时序的,便于快速查找并删除最旧的key。

  • e.希望能够快速删除key列表中的任何一个key,因为如果频次为freq的某个key被访问,那么它的频次就会变成freq+1,就应该从freq对应的key列表中删除,加到freq+1对应的key的列表中。

HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
int minFreq = 0;

介绍一下这个LinkedHashSet,它满足我们 cde 这几个要求。你会发现普通的链表LinkedList能够满足 cd 这两个要求,但是由于普通链表不能快速访问链表中的某一个节点,所以无法满足 3.5 的要求。

LinkedHashSet顾名思义,是链表和哈希集合的结合体。链表不能快速访问链表节点,但是插入元素具有时序;哈希集合中的元素无序,但是可以对元素进行快速的访问和删除。

那么,它俩结合起来就兼具了哈希集合和链表的特性,既可以在 O(1) 时间内访问或删除其中的元素,又可以保持插入的时序,高效实现 e 这个需求。

put() 流程图:
在这里插入图片描述

删除某个键key肯定是要同时修改三个映射表的,借助minFreq参数可以从FK表中找到freq最小的keyList,根据时序,其中第一个元素就是要被淘汰的deletedKey,操作三个映射表删除这个key即可。

但是有个细节问题,如果keyList中只有一个元素,那么删除之后minFreq对应的key列表就为空了,也就是minFreq变量需要被更新。如何计算当前的minFreq是多少呢?

实际上没办法快速计算minFreq,只能线性遍历FK表或者KF表来计算,这样肯定不能保证 O(1) 的时间复杂度。

但是,其实这里没必要更新minFreq变量,因为你想想removeMinFreqKey这个函数是在什么时候调用?在put方法中插入新key时可能调用。而你回头看put的代码,插入新key时一定会把minFreq更新成 1,所以说即便这里minFreq变了,我们也不需要管它。

更新某个key的freq肯定会涉及FK表和KF表,所以我们分别更新这两个表就行了。

和之前类似,当FK表中freq对应的列表被删空后,需要删除FK表中freq这个映射。如果这个freq恰好是minFreq,说明minFreq变量需要更新。

能不能快速找到当前的minFreq呢?这里是可以的,因为我们刚才把key的freq加了 1 嘛,所以minFreq也加 1 就行了。

至此,经过层层拆解,LFU 算法就完成了。

// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译。
// 本代码的正确性已通过力扣验证,如有疑问,可以对照我的 java 代码查看。

#include <unordered_map>
#include <unordered_set>
#include <list>
using namespace std;

class LFUCache {
    // key 到 val 的映射,我们后文称为 KV 表
    unordered_map<int, int> keyToVal;
    // key 到 freq 的映射,我们后文称为 KF 表
    unordered_map<int, int> keyToFreq;
    // freq 到 key 列表的映射,我们后文称为 FK 表
    unordered_map<int, list<int>> freqToKeys;
    // 记录最小的频次
    int minFreq;
    // 记录 LFU 缓存的最大容量
    int cap;

public:
    LFUCache(int capacity) {
        this->cap = capacity;
        this->minFreq = 0;
    }

    int get(int key) {
        if (keyToVal.find(key) == keyToVal.end()) {
            return -1;
        }
        // 增加 key 对应的 freq
        increaseFreq(key);
        return keyToVal[key];
    }

    void put(int key, int val) {
        if (this->cap <= 0) return;

        // 若 key 已存在,修改对应的 val 即可
        if (keyToVal.find(key) != keyToVal.end()) {
            keyToVal[key] = val;
            // key 对应的 freq 加一
            increaseFreq(key);
            return;
        }

        // key 不存在,需要插入
        // 容量已满的话需要淘汰一个 freq 最小的 key
        if (this->cap <= keyToVal.size()) {
            removeMinFreqKey();
        }

        // 插入 key 和 val,对应的 freq 为 1
        // 插入 KV 表
        keyToVal[key] = val;
        // 插入 KF 表
        keyToFreq[key] = 1;
        // 插入 FK 表
        freqToKeys[1].push_back(key);
        // 插入新 key 后最小的 freq 肯定是 1
        this->minFreq = 1;
    }

private:
    void increaseFreq(int key) {
        int freq = keyToFreq[key];
        // 更新 KF 表
        keyToFreq[key] = freq + 1;
        // 更新 FK 表
        // 将 key 从 freq 对应的列表中删除
        freqToKeys[freq].remove(key);
        // 将 key 加入 freq + 1 对应的列表中
        freqToKeys[freq + 1].push_back(key);
        // 如果 freq 对应的列表空了,移除这个 freq
        if (freqToKeys[freq].empty()) {
            freqToKeys.erase(freq);
            // 如果这个 freq 恰好是 minFreq,更新 minFreq
            if (freq == this->minFreq) {
                this->minFreq++;
            }
        }
    }

    void removeMinFreqKey() {
        // freq 最小的 key 列表
        auto& keyList = freqToKeys[this->minFreq];
        // 其中最先被插入的那个 key 就是该被淘汰的 key
        int deletedKey = keyList.front();
        // 更新 FK 表
        keyList.pop_front();
        if (keyList.empty()) {
            freqToKeys.erase(this->minFreq);
            // 问:这里需要更新 minFreq 的值吗?
        }
        // 更新 KV 表
        keyToVal.erase(deletedKey);
        // 更新 KF 表
        keyToFreq.erase(deletedKey);
    }
};

方法三:双哈希表
/*
freq_table: 以频率 freq 为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq
key_table: 以键值 key 为索引,每个索引存放对应缓存在 freq_table 中链表里的内存地址
minfreq: 同时需要记录一个当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。
*/

// 缓存的节点信息
struct Node {
    int key, val, freq;
    Node(int key, int val, int freq): key(key), val(val), freq(freq){}
};
class LFUCache {
    int minfreq; // 当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。
    int capacity;
    unordered_map<int, list<Node>::iterator> key_table;
    unordered_map<int, list<Node>> freq_table;
public:
    LFUCache(int _capacity) {
        minfreq = 0;
        capacity = _capacity;
        key_table.clear();
        freq_table.clear();
    }
    
    int get(int key) {
        if (capacity == 0) {
            return -1;
        }

        auto it = key_table.find(key);
        if (it == key_table.end()) {
            return -1;
        }

        list<Node>::iterator node = it->second;
        int val = node->val
        int freq = node->freq;
        freq_table[freq].erase(node);

        // 如果当前链表为空,我们需要在哈希表中删除,且更新minFreq
        if (freq_table[freq].size() == 0) {
            freq_table.erase(freq);
            if (minfreq == freq) {
                minfreq += 1;
            }
        }
        // 插入到 freq + 1 中
        freq_table[freq + 1].push_front(Node(key, val, freq + 1));
        key_table[key] = freq_table[freq + 1].begin();
        return val;
    }
    
    void put(int key, int value) {
        if (capacity == 0) {
            return;
        }

        auto it = key_table.find(key);
        if (it == key_table.end()) {
            // 缓存已满,需要进行删除操作
            if (key_table.size() == capacity) {
                // 通过 minFreq 拿到 freq_table[minFreq] 链表的末尾节点
                auto it2 = freq_table[minfreq].back();
                key_table.erase(it2.key);
                freq_table[minfreq].pop_back();
                if (freq_table[minfreq].size() == 0) {
                    freq_table.erase(minfreq);
                }
            } 
            freq_table[1].push_front(Node(key, value, 1));
            key_table[key] = freq_table[1].begin();
            minfreq = 1;
        } else {
            // 与 get 操作基本一致,除了需要更新缓存的值
            list<Node>::iterator node = it->second;
            int freq = node->freq;
            freq_table[freq].erase(node);
            if (freq_table[freq].size() == 0) {
                freq_table.erase(freq);
                if (minfreq == freq) {
                    minfreq += 1;
                }
            }
            freq_table[freq + 1].push_front(Node(key, value, freq + 1));
            key_table[key] = freq_table[freq + 1].begin();
        }
    }
};

在这里插入图片描述

在这里插入图片描述
更新的时候为什么是插入到链表头,这其实是为了保证缓存在当前链表中从链表头到链表尾的插入时间是有序的,为下面的删除操作服务。

对于 put(key, value) 操作,我们先通过索引 key在 key_table 中查看是否有对应的缓存,如果有的话,其实操作等价于 get(key) 操作,唯一的区别就是我们需要将当前的缓存里的值更新为 value。如果没有的话,相当于是新加入的缓存,如果缓存已经到达容量,需要先删除最近最少使用的缓存,再进行插入。

先考虑插入,由于是新插入的,所以缓存的使用频率一定是 1,所以我们将缓存的信息插入到 freq_table 中 1 索引下的列表头即可,同时更新 key_table[key] 的信息,以及更新 minFreq = 1。

那么剩下的就是删除操作了,由于我们实时维护了 minFreq,所以我们能够知道 freq_table 里目前最少使用频率的索引,同时因为我们保证了链表中从链表头到链表尾的插入时间是有序的,所以 freq_table[minFreq] 的链表中链表尾的节点即为使用频率最小且插入时间最早的节点,我们删除它同时根据情况更新 minFreq ,整个时间复杂度均为 O(1)。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

斯坦福泡茶机器人DexCap源码解析:涵盖收集数据、处理数据、模型训练三大阶段

前言 因为我司「七月在线」关于dexcap的复现/优化接近尾声了&#xff0c;故准备把dexcap的源码也分析下。​下周则分析下iDP3的源码——为队伍「iDP3人形的复现/优化」助力 最开始&#xff0c;dexcap的源码分析属于此文《DexCap——斯坦福李飞飞团队泡茶机器人&#xff1a;带…

DICOM标准:DICOM医学影像中的覆盖层(Overlay)概念详解

引言 DICOM&#xff08;数字成像和通信医学&#xff09;标准在医学影像的存储、传输和交换中起着关键作用。覆盖层&#xff08;Overlay&#xff09;作为DICOM标准中的一个重要组成部分&#xff0c;用于在医学影像上叠加图形信息&#xff0c;如注释、标记、测量结果等。本文将深…

Windows搭建流媒体服务并使用ffmpeg推流播放rtsp和rtmp流

文章目录 搭建流媒体服务方式一安装mediamtx启动meidamtx关闭meidamtx 方式二安装ZLMediaKit启动ZLMediaKit关闭ZLMediaKit 安装FFmpeg进行推流使用FFmpeg进行rtmp推流使用VLC播放rtmp流停止FFmpeg的rtmp推流使用FFmpeg进行rtsp推流使用VLC播放rtmp流停止FFmpeg的rtsp推流 本文…

[ Linux 命令基础 5 ] Linux 命令详解-网络管理命令

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

深入浅出WebSocket(实践聊天室demo)

文章目录 什么是WebSocket?WebSocket连接过程WebSocket与Http的区别重连机制完整代码使用方法心跳机制实现聊天室demo(基于Socket.io)参考文章、视频小广告~什么是WebSocket? WebSocket 是一种在单个TCP连接上进行全双工通信的协议(计算机网络应用层的协议) 在 WebSocket A…

时序预测 | 改进图卷积+informer时间序列预测,pytorch架构

时序预测 | 改进图卷积informer时间序列预测&#xff0c;pytorch架构 目录 时序预测 | 改进图卷积informer时间序列预测&#xff0c;pytorch架构预测效果基本介绍参考资料 预测效果 基本介绍 改进图卷积informer时间序列预测代码 CTR-GC卷积,informer&#xff0c;CTR-GC 图卷积…

vue+Leaflet.PM插件实现创建和编辑几何图形(点、线、面、圆等)

场景 VueLeaflet实现加载OSM显示地图&#xff1a;https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/122317394在上面加载显示OSM的基础上&#xff0c;使用Leaflet.pm插件实现在页面上绘制、编辑、剪切、移动几何元素。Leaflet.pm插件 用于创建和编辑几何图层的插件可…

网站架构知识之Ansible进阶2(day023)

1.include文件 应用场景: 1个ansible剧本内容过多,涉及到多个play(- host:web),可读性变弱&#xff0c;不方便调试。 于是人们想出把单个大的剧本拆分为多个小的剧本&#xff0c; 多个小的剧本可以通过include功能合并使用。 使用方法&#xff0c;书写好对应的剧本文件&#…

订单日记助力“实峰科技”提升业务效率

感谢北京实峰科技有限公司选择使用订单日记&#xff01; 北京实峰科技有限公司&#xff0c;成立于2022年&#xff0c;位于北京市石景区&#xff0c;是一家以从事生产、销售微特电机、输配电及控制设备等业务为主的企业。 在业务不断壮大的过程中&#xff0c;想使用一种既能提…

论文阅读:DualDn Dual-domain Denoising via Differentiable ISP

这篇文章是 2024 ECCV 的一篇文章&#xff0c;介绍的是降噪相关的工作。 Abstract 图像去噪是相机图像信号处理 (ISP) 流程中的一个关键组成部分。将去噪器融入 ISP 流程有两种典型方式&#xff1a;直接对拍摄的原始帧&#xff08;RAW域&#xff09;应用去噪器&#xff0c;或…

详解MySQL安装

目录 Ubantu 1. 使⽤apt安装MySQL 2.查看MySQL状态 3. MySQL 安装安全设置 4.设置密码 卸载MySQL Centos 1. 确认当前的系统版本 2.下载MySQL源 3.安装MySQL 4.启动mysqld 5.查看MySQL状态 6.设置开机自启动 7.查看MySQL密码&#xff0c;并登录 8.修改密码 Ubant…

AndroidStudio-视图基础

一、设置视图的宽高 1.在XML文件中设置视图宽高 视图宽度通过属性android:layout_width表达&#xff0c;视图高度通过属性android:layout_height表达&#xff0c;宽高的取值主要有下列三种: &#xff08;1&#xff09;wrap_content:表示与内容自适应。对于文本视图来说&…

【LQ_tips】在DEVc++中的带空格的输入格式

目标输入&#xff1a; 3 4 5 6 关于cin.ignore();的解释&#xff1a; 在 DEV C 或任何其他 C 环境中&#xff0c;如果你的代码没有输出&#xff0c;这可能是由于输入缓冲区的问题。当你使用 cin 读取输入时&#xff0c;如果输入中包含空格&#xff0c;cin 会停止读取。因此&a…

dolphin 配置data 从文件导入hive 实践(一)

datax 支持多种数据源的相互读写&#xff0c;作为开源软件&#xff0c;提供了离线采集功能&#xff0c;方便系统开发&#xff0c;过程中遇到诸多配置&#xff0c;需要开发者自己探索&#xff0c;免费同样有成本 配置模板 {"setting": {},"job": {"s…

计算机网络综合题

IP数据报的划分 CRC差错检测 冗余码的计算 因此&#xff0c;余数是1110&#xff0c;传输的数为11010110111110。在传输过程中最后两位变成o&#xff0c;接收端能够发现&#xff0c;因为11010110111110除以10011余数不为0。 子网划分 暴力求解法 &#xff08;定长子网划分大量…

Linux系统程序设计--2. 文件I/O

文件I/O 标准C的I/O FILE结构体 下面只列出了5个成员 可以观察到&#xff0c;有些函数没有FILE类型的结构体指针例如printf主要是一些标准输出&#xff0c;因为其内部用到了stdin&#xff0c;stdout&#xff0c;stderr查找文件所在的位置:find \ -name stat.h查找头文件所…

Modbus TCP 西门子PLC与 多个设备进行通讯 使用Modbus Slave模拟多个设备ID

目录 1前言 2相同地址不同ID 1创建连接数据 2创建连接程序 3模块参数设置 4Modbus Slave设置 5成果展示 3结语 1前言 本篇文章讲了PLC如何与同一地址的多个ID设备进行通讯&#xff0c;如果看不懂这篇文章就去看一下这篇博客学一下基础。 Modbus TCP 西门子PLC指令以太…

group_concat配置影响程序出bug

在 ThinkPHP 5 中&#xff0c;想要临时修改 MySQL 数据库的 group_concat_max_len 参数&#xff0c;可以使用 原生 SQL 执行 来修改该值。你可以通过 Db 类来执行 SQL 语句&#xff0c;从而修改会话&#xff08;Session&#xff09;级别的变量。 步骤 设置 group_concat_max_l…

UnixBench和Geekbench进行服务器跑分

1 概述 服务器的基准测试&#xff0c;常见的测试工具有UnixBench、Geekbench、sysbench等。本文主要介绍UnixBench和Geekbench。 1.1 UnixBench UnixBench是一款开源的测试UNIX系统基本性能的工具&#xff08;https://github.com/kdlucas/byte-unixbench&#xff09;&#x…

皮卡超级壁纸 1.4.1 | 解锁会员版的全景壁纸、动态壁纸和超级壁纸

皮卡超级壁纸是一款提供海量壁纸的应用&#xff0c;不仅包含静态的精美壁纸&#xff0c;还提供了独特的超级壁纸。这些超级壁纸不仅仅是动态效果&#xff0c;还能自动匹配用户的手机UI&#xff0c;提供更加个性化的体验。解锁会员版后&#xff0c;用户可以享受更多高级功能和壁…