LFU缓存策略算法

article/2025/10/17 3:27:47

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

1 LFU基本介绍

LFU,全称是Least Frequently Used,最不经常使用策略,在一段时间内,数据被使用频次最少的,优先被淘汰。维基百科中这样介绍:最少使用(LFU)是一种用于管理计算机内存的缓存算法,主要是记录和追踪内存块的使用次数,当缓存已满并且需要更多空间时,系统将以最低内存块使用频率清除内存。采用LFU算法的最简单方法是为每个加载到缓存的块分配一个计数器,每次引用该块时,计数器将增加一,当缓存达到容量并有一个新的内存块等待插入时,系统将搜索计数器最低的块并将其从缓存中删除。
在这里插入图片描述
上面这个图就是一个LFU的简单实现思路:

  1. 在链表的开始插入元素,每插入一次计数一次
  2. 按照次数重新排序链表,
  3. 如果次数相同的话,按照插入时间排序,然后从链表尾部选择淘汰的数据

基于以上的思路我们接下来设计LFU算法。

2 LFU算法实现

2.1 Node节点定义

  • 首先Node需要包含了key和value,主要用来存放相应的数据,必不可少;
  • 基于以上的思路,LFU的主要实现思想是比较访问的次数,如果在次数相同的情况下需要比较节点的时间,越早放入的越快被淘汰,因此我们需要在Node节点上加入time和count的属性,分别用来记录节点的访问的时间和访问次数;
  • 因为要比较时间和次数,所以我们需要一个比较的方法,所以Node节点需要实现comparable接口,并重写了compareTo方法,方法里的具体逻辑为首先比较节点的访问次数,在访问次数相同的情况下比较节点的访问时间,这样在排序方法里面通过比较key来选择淘汰的key。

有了上面的分析,我们给出定义好的Node节点的代码,如下:

/*** @author likangmin* @version 1.0* @create 2021/3/24 14:29* @desc*/
public class Node implements Comparable<Node> {/*** 键*/Object key;/*** 值*/Object value;/*** 访问时间*/long time;/*** 访问次数*/int count;public Node(Object key, Object value, long time, int count) {this.key = key;this.value = value;this.time = time;this.count = count;}public Object getKey() {return key;}public void setKey(Object key) {this.key = key;}public Object getValue() {return value;}public void setValue(Object value) {this.value = value;}public long getTime() {return time;}public void setTime(long time) {this.time = time;}public int getCount() {return count;}public void setCount(int count) {this.count = count;}@Overridepublic int compareTo(Node o) {int compare = Integer.compare(this.count, o.count);//在数目相同的情况下 比较时间if (compare == 0) {return Long.compare(this.time,o.time);}return compare;}}

2.2 LFU类的定义

定义LFU类,这里采用泛型,声明为K和V,还有总容量和一个Map(caches)用来维护所有的节点。在构造方法里将size传递进去,并且创建了一个LinkedHashMap,采用linkedHashMap的主要原因是维护key的顺序。对应的代码如下:

/*** @author likangmin* @version 1.0* @create 2021/3/24 14:51* @desc*/
public class LFU<K, V> {/*** 总容量*/private int capacity;/*** 所有的node节点*/private Map<K, Node> caches;public Map<K, Node> getCaches() {return caches;}public void setCaches(Map<K, Node> caches) {this.caches = caches;}/*** 构造方法* @param size*/public LFU(int size) {this.capacity = size;caches = new LinkedHashMap<K, Node>(size);}}

有了类的定义之后,我们接下来需要设计一些方法,包括添加元素,删除元素,获取元素等。

2.3 添加元素

添加元素的逻辑主要如下:

  1. 先从缓存中根据key获取节点,如果获取不到,说明是新添加的元素,然后和容量比较,大于预定容量的话,需要找出count计数最小(计数相同的情况下,选择时间最久)的节点,然后移除掉那个;
  2. 如果在预定的大小之内,就新创建节点,注意这里不能使用 System.currentTimeMillis()方法,因为毫秒级别的粒度无法对插入的时间进行区分,在运行比较快的情况下,只有System.nanoTime()才可以将key的插入时间区分,默认设置count计数为1;
  3. 如果能获取到,表示是旧的元素,那么就用新值覆盖旧值,计数+1,设置key的time为当前纳秒时间;
  4. 最后还需要进行排序

从以上步骤可以看出插入元素的逻辑主要是添加进入缓存,更新元素的时间和计数,重新排序,流程图如下:
在这里插入图片描述
每次put或者get元素都需要进行排序,排序的主要意义在于按照key的cout和time进行一个key顺序的重组,这里的逻辑是首先将缓存map创建成一个list,然后按照Node的value进行重组整个map。然后将原来的缓存清空,遍历这个map, 把key和value的值放进去原来的缓存中的顺序就进行了重组。淘汰最小的元素这里调用了Collections.min方法,然后通过比较key的compareTo方法,找到计数最小和时间最长的元素,直接从缓存中移除。有了上面的分析,我们直接写出对应的方法:

/*** 添加元素* @param key* @param value*/public void put(K key, V value) {Node node = caches.get(key);//如果新元素if (node == null) {//如果超过元素容纳量if (caches.size() >= capacity) {//移除count计数最小的那个keyK leastKey = removeLeastCount();caches.remove(leastKey);}//创建新节点node = new Node(key, value, System.nanoTime(), 1);caches.put(key, node);} else {//已经存在的元素覆盖旧值node.value = value;node.setCount(node.getCount() + 1);node.setTime(System.nanoTime());}sort();}/*** 移除统计数或者时间比较最小的那个* @return*/private K removeLeastCount() {Collection<Node> values = caches.values();Node min = Collections.min(values);return (K) min.getKey();}/*** 排序*/private void sort() {List<Map.Entry<K, Node>> list = new ArrayList<>(caches.entrySet());Collections.sort(list, (o1, o2) -> o2.getValue().compareTo(o1.getValue()));caches.clear();for (Map.Entry<K, Node> kNodeEntry : list) {caches.put(kNodeEntry.getKey(), kNodeEntry.getValue());}}

2.4 获取元素

获取元素的逻辑主要如下:

  1. 首先是从缓存map中获取,如果不存在则返回null,
  2. 如果存在,在获取到元素之后需要进行节点的更新,计数+1和刷新节点的时间
  3. 根据LFU的原则,在当前时间获取到这个节点以后,这个节点就暂时变成了热点节点,但是它的count计数也有可能是小于某个节点的count的,所以此时不能将它直接移动到链表顶,还需要进行一次排序,重组它的位置

我们依然可以画出对应的流程图:
在这里插入图片描述
有了上面的分析,我们直接写出对应的方法:

/*** 获取元素* @param key* @return*/public V get(K key) {Node node = caches.get(key);if (node != null) {node.setCount(node.getCount() + 1);node.setTime(System.nanoTime());sort();return (V) node.value;}return null;}

3 LFU算法测试

  • 我们首先声明一个LRU,然后默认最大的大小为5,依次put进入A、B、C、D、E、F6个元素,此时将会找到计数最小和时间最短的元素,那么将会淘汰A(因为count值都是1);
  • 记着get两次B元素,那么B元素的count=3,时间更新为最新。此时B将会移动到顶;
  • 接着在getC元素,C元素的count=2,时间会最新,那么此时因为它的count值依然小于B,所以它依然在B后面;
  • 再getF元素,F元素的count=2,又因为它的时间会最新,所以在与C相同的计数下,F元素更新(时间距离现在最近),所以链表将会移动,F会在C的前面;
  • 再次put一次C,此时C的count=3,同时时间为最新,那么此刻C的count和B保持一致,则他们比较时间,C明显更新,所以C将会排在B的前面;
  • 最终的顺序应该是:C->B->F->E->D。

代码如下:

/*** @author likangmin* @version 1.0* @create 2021/3/24 14:57* @desc*/
public class MyLFUTest {public static void main(String[] args) {LFU<Integer,String> lruCache = new LFU<>(5);lruCache.put(1, "A");lruCache.put(2, "B");lruCache.put(3, "C");lruCache.put(4, "D");lruCache.put(5, "E");lruCache.put(6, "F");lruCache.get(2);lruCache.get(2);lruCache.get(3);lruCache.get(6);//重新put节点3lruCache.put(3,"C");final Map<Integer, Node> caches = (Map<Integer, Node>) lruCache.getCaches();for (Map.Entry<Integer, Node> nodeEntry : caches.entrySet()) {System.out.println(nodeEntry.getValue().value);}}
}

我们启动程序看一下输出,可以看到正如我们分析的一样。
在这里插入图片描述

4 LRU和LFU的区别

LRU和LFU侧重点不同,LRU主要体现在对元素的使用时间上,而LFU主要体现在对元素的使用频次上。LFU的缺陷是:在短期的时间内,对某些缓存的访问频次很高,这些缓存会立刻晋升为热点数据,而保证不会淘汰,这样会驻留在系统内存里面。而实际上,这部分数据只是短暂的高频率访问,之后将会长期不访问,瞬时的高频访问将会造成这部分数据的引用频率加快,而一些新加入的缓存很容易被快速删除,因为它们的引用频率很低。

最后,大家可以跟我之前的文章如何设计LRU Cache算法对照着来看,相信看完你会对这两种缓存策略有更深刻的理解,下次不管是跟朋友谈论还是面试,你都可以在他们面前show一波了。


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

相关文章

国内编程学习网站

在本文中&#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;所以我就结合自己的亲身体会来…

java编程自学app_Java编程自学软件

Java编程自学软件是是一款Java学习软件。Java编程自学软件为用户提供Java语言&#xff0c;ISh和SQL 数据库编程等技术方便用户学习Java知识。有需要自学Java编程的小伙伴们可在华军软件园下载Java编程自学软件。 Java编程自学软件功能特色 专业化、具体化。 有真正意义上的实战…

c语言 软件编程入门自学,软件编程入门自学

文章目录[隐藏] 软件编程入门自学 作为界面&#xff0c;MFC方便上手&#xff0c;QT也不错。您好&#xff0c;自学编程建议从C语言开始。可以说60%~80%的程序员都是从C语言开始的。 众所周知&#xff0c;编程语言分为结构化编程语言和面向对象编程语言。结构化编程语言比面向对象…

自学编程,收藏好这7个免费网站,可省你上万块钱的学费

如果你要自学编程&#xff0c;一定要收藏好这7个网站&#xff0c;上面免费的优质教程很多&#xff0c;完全可以省去你上万块钱的学费&#xff01; 话不多说&#xff0c;直接上干货&#xff01; 第一个&#xff0c;W3school 一个主打图文教程的网站&#xff0c;不管是前端开发…

蛙跳算法优化VMD参数,惩罚系数,分解层数,matlab语言 ,最小包络熵为适应度函数。

蛙跳算法优化VMD参数&#xff0c;惩罚系数&#xff0c;分解层数&#xff0c;matlab语言 &#xff0c;最小包络熵为适应度函数。

粒子群算法(6)-----几个适应度评价函数

下面给出几个适应度评价函数&#xff0c;并给出图形表示 头几天机子种了病毒&#xff0c;重新安装了系统&#xff0c;不小心把程序全部格式化了&#xff0c;痛哭&#xff01;&#xff01;&#xff01;没办法&#xff0c;好多程序不见了&#xff0c;现在把这几个典型的函数重新编…

粒子群算法几个适应度评价函数

http://blog.csdn.net/niuyongjie/article/details/1619496 粒子群算法(6)-----几个适应度评价函数 标签&#xff1a; 算法图形function 2007-05-21 16:28 37960人阅读 评论(25) 收藏 举报 分类&#xff1a; 粒子群算法研究&#xff08;8&#xff09; 版权声明&#xff1…

遗传算法优化LSTM网络结构(实现自动根据适应度函数:即准确率来全局搜索最佳网络结构):主要被优化参数:网络层数,每层的神经元个数,全连接的层数,全连接层的神经元个数。代码有详细注解

代码视频链接:https://www.bilibili.com/video/BV19q4y1Q7DR/ 代码效果图: 1.优化参数 本文优化的是LSTM的层数参数和各层神经元参数,其中包含了lstm层和Dense层,其中我们规定了神经网络的层数不超过3层,每层的神经元个数在[32,256]之间。 2.注意事项 2.1.本文的遗传算…

粒子群算法的几个适应度评价函数

下面给出几个适应度评价函数&#xff0c;并给出图形表示 第一个函数&#xff1a;Griewank函数&#xff0c;图形如下所示&#xff1a; 适应度函数如下&#xff1a;&#xff08;为了求最大值&#xff0c;我去了所有函数值的相反数&#xff09; function y Griewank(x) % Griew…

【人工智能】人工智能二——遗传算法的基本概念遗传算法的基本算法(编码群体设定适应度函数选择交叉变异遗传算法步骤)解决带约束的函数优化问题多目标的遗传算法遗传算法的改进算法

人工智能二——遗传算法的基本概念&遗传算法的基本算法&#xff08;编码&群体设定&适应度函数&选择&交叉&变异&遗传算法步骤&#xff09;&解决带约束的函数优化问题&多目标的遗传算法&遗传算法的改进算法 遗传算法的基本概念遗传算法的…

【建模必备】遗传算法的基本原理与步骤(适应度函数与适应度分配)

如果喜欢这里的内容&#xff0c;你能够给我最大的帮助就是转发&#xff0c;告诉你的朋友&#xff0c;鼓励他们一起来学习。 If you like the content here, you can give me the greatest help is forwarding, tell your friends, encourage them to learn together.

2018-3-19 损失函数与适应度函数,稳定选择与分裂选择

1.适应度与损失函数 我觉的&#xff1a; &#xff08;1&#xff09;都是用来描述目标函数一个方面的效能的一个函数 &#xff08;2&#xff09;进行输入之后&#xff0c;结果都是一个可以进行比较的值 来源&#xff1a;机器学习之 损失函数和风险函数 - CSDN博客 http://bl…

遗传算法(2):对适应度函数的改进

Review&#xff1a; 基本遗传算法 ----------------------------- 关于适应度的问题 1. 有的时候&#xff0c;目标函数可能不一定可以直接作为适应度函数。 2. f(x1), f(x2), ... f(xN)之间的差别可能不是很大&#xff0c;个体被选出的概率差不多&#xff0c;这可能导致GA的选择…

遗传算法原理,交叉、变异、适应度函数的设置

遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09;由霍兰德教授在20世纪70年代提出&#xff0c;是以自然选择和遗传变异为理论依据的全局性概率搜索优化算法模型。采用遗传算法寻优时需要将问题的候选解进行编码&#xff0c;即一个候选解对应一个编码&#xff…

利用遗传算法GA和粒子群算法PSO优化算法,将BP神经网络训练集的MSE作为适应度函数

利用遗传算法GA和粒子群算法PSO优化算法&#xff0c;将BP神经网络训练集的MSE作为适应度函数&#xff0c;获取最优的权值和阈值在反向输入到BP神经网络里构建回归预测模型&#xff0c;同时能够打印出模型的多个评价指标&#xff0c;具体效果可以看图 ID:3250669194443543Matl…

麻雀算法SSA,优化VMD,适应度函数为最小包络熵,包含MATLAB源代码

针对大家评论区给出的很多问题&#xff0c;作者一直都有关注&#xff0c;因此在这里又写了一篇文章&#xff0c;而且思路与这篇文章有不同之处&#xff0c;至于具体的不同之处放在下一篇文章了&#xff0c;大家感兴趣的可以移步观看&#xff0c;下一篇文章可以说是作者的呕心力…

计算适应度函数(目标函数)(单目标)

适应度函数 function fitness = CacFitNess(Energy,Time,MissError,overSpeed,Jerk) %UNTITLED 计算适应度函数 caculation fitness % 能耗Energy % 时间Time % 舒适度Jerk % 超限速overSpeed % 停车误差MissError global DESINTIME EMAX; y=zeros(1,5);%% 1 能耗 if E…