LFU缓存详解

article/2025/10/16 23:41:38

LFU缓存详解

文章目录

  • LFU缓存详解
    • 一、LFU概念
    • 二、分析
      • 方法一:哈希表 + 平衡二叉树
      • 方法二:双哈希表

一、LFU概念

LRU详解:https://blog.csdn.net/wolfGuiDao/article/details/105862106

  • LRU 算法相当于把数据按照时间排序,这个需求借助链表很自然就能实现,你一直从链表头部加入元素的话,越靠近头部的元素就是新的数据,越靠近尾部的元素就是旧的数据,我们进行缓存淘汰的时候只要简单地将尾部的元素淘汰掉就行了。

  • LFU 算法相当于是淘汰访问频次最低的数据如果访问频次最低的数据有多条,需要淘汰最旧的数据。把数据按照访问频次进行排序,而且频次还会不断变化,这可不容易实现。

  • 从题面上感觉并不难,不就是排个序嘛,如果这么想就错了:因为要求O(1) 时间复杂度内执行两项操作

在这里插入图片描述
在这里插入图片描述

二、分析

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

  • 因为我们刚刚分析过,LFU算法关系到数据的访问频次和数据的新旧(即数据的访问时间),所以我们封装一个数据结构:
struct Node 
{//数据的访问频次int cnt;//数据的访问时间int time;//数据int key, value;// 我们需要实现一个 Node 类的比较函数// 将 cnt(使用频率)作为第一关键字,time(最近一次使用的时间)作为第二关键字// 因为我们要在数据频率相等时淘汰数据旧的,也就是时间小的bool operator< (const Node& rhs) const {return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;}
};
  • 比较直观的想法就是我们用哈希表 key_table 以键 key 为索引存储缓存根据 (cnt,time) 双关键字建立一个平衡二叉树 S 来保持缓存
  • 由于在 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 插入新的缓存信息即可。
    在这里插入图片描述
  • 完整代码
struct Node 
{//频率int cnt;//时间int time;//数据int key, value;//构造Node(int _cnt, int _time, int _key, int _value):cnt(_cnt), time(_time), key(_key), value(_value){}//自定义比较规则:以频率为第一关键字,时间为第二关键字bool operator < (const Node& rhs) const {return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;}
};class LFUCache 
{// 缓存容量int capacity;//时间戳int time;//关键字和数据信息的哈希索引unordered_map<int, Node> key_table;//平衡搜索树set<Node> S;public:LFUCache(int _capacity) {capacity = _capacity;time = 0;key_table.clear();S.clear();}int get(int key) {if (capacity == 0) return -1;auto it = key_table.find(key);// 如果哈希表中没有键 key,返回 -1if (it == key_table.end()) return -1;// 从哈希表中得到旧的缓存Node cache = it -> second;// 从平衡二叉树中删除旧的缓存S.erase(cache);// 将旧缓存更新cache.cnt += 1;cache.time = ++time;// 将新缓存重新放入哈希表和平衡二叉树中S.insert(cache);//更新哈希表当中key对应的缓存数据it -> second = cache;//返回return cache.value;}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) {// 从哈希表和平衡二叉树中删除最近最少使用的缓存key_table.erase(S.begin() -> key);S.erase(S.begin());}// 创建新的缓存Node cache = Node(1, ++time, key, value);// 将新缓存放入哈希表和平衡二叉树中key_table.insert(make_pair(key, cache));S.insert(cache);}else {// 这里和 get() 函数类似//代表找到了key,取对应的缓存数据Node cache = it -> second;//在红黑树S中删除旧的缓存数据S.erase(cache);//在旧的缓存基础上更新缓存数据cache.cnt += 1;cache.time = ++time;cache.value = value;//重新把缓存数据插到红黑树S当中S.insert(cache);//跟新哈希表对应key的缓存数据it -> second = cache;}}
};

方法二:双哈希表

  • 我们定义两个哈希表

  • 第一个 freq_table 以频率 freq 为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq。

  • 第二个 key_table 以键值 key 为索引,每个索引存放对应缓存在 freq_table 中链表里的内存地址,这样我们就能利用两个哈希表来使得两个操作的时间复杂度均为 O(1)。

  • 同时需要记录一个当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。

  • 对于 get(key) 操作,我们能通过索引 key 在 key_table 中找到缓存在 freq_table 中的链表的内存地址,如果不存在直接返回 -1,否则我们能获取到对应缓存的相关信息,这样我们就能知道缓存的键值还有使用频率,直接返回 key 对应的值即可。

  • 但是我们注意到 get 操作后这个缓存的使用频率加一了,所以我们需要更新缓存在哈希表 freq_table 中的位置。

  • 已知这个缓存的键 key,值 value,以及使用频率 freq,那么该缓存应该存放到 freq_table 中 freq + 1 索引下的链表中

  • 所以我们在当前链表中 删除该缓存对应的节点,根据情况更新 minFreq 值,然后将其插入到 freq + 1 索引下的链表头完成更新。这其中的操作复杂度均为 O(1)。

  • 你可能会疑惑更新的时候为什么是插入到链表头,这其实是为了保证缓存在当前链表中从链表头到链表尾的插入时间是有序的,为下面的删除操作服务。

  • 对于 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)。

  • 完整代码

// 缓存的节点信息
struct Node 
{//数据int key, val;//频率int freq;Node(int _key,int _val,int _freq): key(_key), val(_val), freq(_freq){}};
class LFUCache 
{//缓存的最小频率int minfreq;//容量int capacity;//哈希表;key为关键字key,value为key在freq_table中的位置unordered_map<int, list<Node>::iterator> key_table;//哈希表;key为缓存数据的频率,value为缓存数据的链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;//代表找到了,取key对应的缓存数据在freq_table中某条链上的位置list<Node>::iterator node = it -> second;//保存旧的缓存数据int val = node -> val, freq = node -> freq;//把旧的缓存数据从freq_table对应频率的链上删除freq_table[freq].erase(node);// 如果删除后,当前链表为空,我们需要在哈希表中删除,且更新minFreqif (freq_table[freq].size() == 0) {freq_table.erase(freq);if (minfreq == freq) minfreq += 1;}// 插入到 freq + 1 中频率的链头,因为该缓存数据get了一次freq_table[freq + 1].push_front(Node(key, val, freq + 1));//同时更新该缓存数据在key_table中的信息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和freq_table中淘汰掉key_table.erase(it2.key);freq_table[minfreq].pop_back();//如果删除后对应频率的链表为空,也需要在freq_table中删除if (freq_table[minfreq].size() == 0) {freq_table.erase(minfreq);}}//执行插入缓存操作:在freq_table中频率为1的链头插入新的缓存数据 freq_table[1].push_front(Node(key, value, 1));//在key_table中保存该缓存数据在链的迭代器key_table[key] = freq_table[1].begin();//更新最小的频率minfreq = 1;} else {//代表找到了缓存数据// 与 get 操作基本一致,除了需要更新缓存的值//取该缓存数据在freq_table中某条链的迭代器list<Node>::iterator node = it -> second;//从迭代器中取到该缓存的频率int freq = node -> freq;//从freq_table中对应链删除该缓存freq_table[freq].erase(node);//同样需要判断删除后该链是否为空if (freq_table[freq].size() == 0) {freq_table.erase(freq);if (minfreq == freq) minfreq += 1;}//把该缓存数据放大freq_table的频率 + 1 的链头freq_table[freq + 1].push_front(Node(key, value, freq + 1));//同时更新该缓存数据在key_table中的位置key_table[key] = freq_table[freq + 1].begin();}}
};

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


http://chatgpt.dhexx.cn/article/xNRNnvSN.shtml

相关文章

LRU和LFU算法

LRU淘汰算法 LRU&#xff1a;Least Recently Used&#xff0c;最近最少使用。即淘汰掉最近最少使用的数据。 以时间轴作为走向&#xff1a; 如果数据A刚被访问&#xff0c;那么A就应该调至头部&#xff1a; 假如内存容量已满&#xff0c;现在新数据E被插入&#xff0c;需要淘…

FIFO、LRU、LFU的含义和原理

FIFO&#xff1a;First In First Out&#xff0c;先进先出LRU&#xff1a;Least Recently Used&#xff0c;最近最少使用 LFU&#xff1a;Least Frequently Used&#xff0c;最不经常使用 以上三者都是缓存过期策略。 原理和实现&#xff1a; 一、FIFO按照“先进先出&#…

LFU算法

LFU&#xff08;LeastFrequently Used&#xff09;&#xff0c;即最近最多使用算法。它是基于“如果一个数据在最近一段时间内使用次数很少&#xff0c;那么在将来一段时间内被使用的可能性也很小”的思路。LFU算法需要维护一个队列记录所有数据的访问记录&#xff0c;每个数据…

Redis精通系列——LFU算法详述(Least Frequently Used - 最不经常使用)

本文已收录于专栏 ❤️《Redis精通系列》❤️ 上千人点赞收藏&#xff0c;全套Redis学习资料&#xff0c;大厂必备技能&#xff01; 目录 1、简介 2、实现方式 2.1 LRU实现方式 2.2 LFU实现方式 3、LFU使用 3.1 配置文件开启LFU淘汰算法 1、简介 LRU有一个明显的缺点&…

什么是LRU?什么是LFU

缓存算法是指令的一个明细表&#xff0c;用于决定缓存系统中哪些数据应该被删去。 常见类型包括LFU、LRU、ARC、FIFO、MRU。 最不经常使用算法&#xff08;LFU&#xff09;&#xff1a; 这个缓存算法使用一个计数器来记录条目被访问的频率。通过使用LFU缓存算法&#xff0c;…

LRU算法和LFU算法

文章目录 1、LRU算法2、LFU算法 1、LRU算法 LRU是Least Recently Used的缩写&#xff0c;即最近最少使用&#xff0c;是一种常用的页面置换算法&#xff0c;选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段&#xff0c;用来记录一个页面自上次被访问以来所…

LRU算法与LFU算法

LRU算法 LRU是Least Recently Used的缩写&#xff0c;即最近最少使用算法&#xff0c;是操作系统中页面置换算法的一种&#xff0c;由于内存中存放的页数有限&#xff0c;所以将最近没有使用到的算法会移出内存。 LRU原理&#xff1a; LRU原理如下图所示&#xff1a; 它会将最…

【算法】LFU及其优化

文章目录 什么是LFU&#xff1f;设计思路代码实现&#xff08;基础版本&#xff09;参考论文代码实现&#xff08;优化版本&#xff09;区别 什么是LFU&#xff1f; LRU及其实现 上文讲解了LRU&#xff0c;他是一个基于最近是否被访问来做缓存淘汰的策略。 那么今天介绍一个新…

LRU和LFU算法解析

文章目录 LRU和LFU算法解析LRULRU概念LRU算法实现LRU算法描述LRU算法图示LRU C代码代码测试 LFULFU概念LFU算法实现LFU算法描述LFU算法图示LFU C代码代码测试 总结 LRU和LFU算法解析 LRU LRU概念 LRU&#xff08;The Least Recently Used&#xff0c;最近最少未使用&#xf…

LRU和LFU的区别

对于web开发而言&#xff0c;缓存必不可少&#xff0c;也是提高性能最常用的方式。无论是浏览器缓存(如果是chrome浏览器&#xff0c;可以通过chrome:://cache查看)&#xff0c;还是服务端的缓存(通过memcached或者redis等内存数据库)。缓存不仅可以加速用户的访问&#xff0c;…

LFU算法族:LFU算法

LFU算法族相关文章目录汇总&#xff1a; LFU算法&#xff08;本文&#xff09;​​​​​​​ LFU-Aging算法 window-LFU算法 1、原理 LFU&#xff08;Least Frequently Used&#xff09;算法&#xff0c;即最少访问算法&#xff0c;根据访问缓存的历史频率来淘汰数据&…

LFU算法详解

LFU算法&#xff1a;least frequently used&#xff0c;最近最不经常使用算法 对于每个条目&#xff0c;维护其使用次数 cnt、最近使用时间 time。 cache容量为 n&#xff0c;即最多存储n个条目。 那么当我需要插入新条目并且cache已经满了的时候&#xff0c;需要删除一个之…

算法题就像搭乐高:手把手带你拆解 LFU 算法

f学算法认准labuladong 东哥带你手把手撕力扣???? 点击下方卡片即可搜索???? PS&#xff1a;以后每篇文章最后&#xff0c;labuladong 都会推荐一些自己学过的优质技术专栏&#xff0c;供读者参考。 上篇文章 算法题就像搭乐高&#xff1a;手把手带你拆解 LRU 算法 写了…

LRU LFU 概念、底层原理及其实现 超详细~

0. 前置提要 本篇约为8650字&#xff0c;阅读完需要约40~60分钟。主要介绍页面置换算法,LRU和LFU的原理及其实现&#xff0c;对应leetcode140和460&#xff0c;如果能给个赞就更好了^-^。 1.从内存置换算法说起 计算机的运行的程序和数据保存在内存中&#xff0c;内存的空间是有…

如何实现LFU缓存(最近最少频率使用)

目录 1.什么是LFU缓存&#xff1f; 2.LFU的使用场景有哪些&#xff1f; 3.LFU缓存的实现方式有哪些&#xff1f; 4.put/get 函数实现具体功能 1.什么是LFU缓存&#xff1f; LFU缓存是一个具有指定大小的缓存&#xff0c;随着添加元素的增加&#xff0c;达到容量的上限&…

LFU缓存策略算法

在之前的文章中&#xff0c;我们介绍了如何设计一个LRU算法–如何设计LRU Cache算法&#xff0c;今天我们再聊一聊另一种缓存策略LFU。目前博主个人博客已经搭建发布&#xff0c;后期相关文章也会发布在上面&#xff0c;大家有兴趣可以去上面学习&#xff0c;点击即可前往文青乐…

国内编程学习网站

在本文中&#xff0c;我们介绍了来自两岸三地的编程学习网站&#xff0c;通过它们&#xff0c;不仅可以一窥国内App开发的发展现状&#xff0c;而且这些网站各有特点&#xff0c;无论是主打游戏学习还是视频学习&#xff0c;对于想要自学的开发者而言&#xff0c;都是个好去处。…

如何高效的自学编程

现在的社会对于IT人才的需求越来越大&#xff0c;程序员的薪资水平在各个行业中都算比较高的。所以很多人都想往IT行业发展&#xff0c;已经身处这个行业的人也需要不断的学习新的知识&#xff0c;因为IT行业的技术更新实在是太快了&#xff0c;不像传统行业那样是越老越吃香。…

电脑编程自学(零基础自学编程怎么入门)

电脑编程自学入手:确定编程学习的方向。编程语言有多种:php,C++,C,C#,JAVA,Python等,每种语言都有不同的优缺点,可以根据自己的兴趣方向选择一门编程语言作为自己的学习目标。 基础阶段的语法学习。学习任何一门编程语言,都需要掌握其编程的语法规则,可以通过阅读一…

自学编程的 6 个致命误区

嗨&#xff0c;小伙伴们大家好&#xff0c;我是沉默王二。本篇文章来和大家聊聊自学编程中的一些误区——这是我在 B 站上看了羊哥的一期视频后有感而发的文章。因为确实有很多读者也曾私信问过我这些方面的问题&#xff0c;很有代表性&#xff0c;所以我就结合自己的亲身体会来…