理解和实现 LFU 缓存置换算法

ops/2024/9/23 10:18:43/

引言

在计算机科学中,缓存是一种重要的技术,用于提高数据访问速度和系统性能。然而,由于缓存空间有限,当缓存满了之后,就需要一种智能的策略来决定哪些数据应该保留,哪些应该被淘汰。LFU(Least Frequently Used,最少使用)算法就是一种常见的缓存淘汰策略,它基于数据项的访问频率来进行优化管理。

LFU_2">LFU算法简介

LFU算法的核心思想是优先淘汰那些访问频率最低的数据项。在缓存达到容量限制时,LFU算法会移除那些被访问次数最少的缓存条目。如果多个条目的访问次数相同,则根据它们最早被访问的时间进行决策,优先删除最早被访问的条目。

LFU_4">LFU算法的工作原理

LFU算法通常需要两种数据结构来实现:

哈希表:提供O(1)时间复杂度的数据访问和插入。此时哈希表需要两个,一个记录key和当前节点的映射,一个记录频率和节点的映射
双向链表:维护数据项的使用顺序,最近使用的在头部,最久未使用的在尾部。

数据访问和插入的流程如下:

获取数据(Get):从缓存中获取数据,如果数据存在(缓存命中),则更新数据使用的频率,也就是频率+1,同时要删除当前节点之前对应的频率映射;如果数据不存在(缓存未命中),返回 -1。
插入数据(Put):将数据放入缓存,如果数据已经存在,则更新数据值并更新数据使用的频率;如果数据不存在,则将数据插入放入缓存中,并将最低频率设置为1。如果缓存已满,首先需要移除缓存中的元素,然后再插入。

LFU_12">LFU算法的实现

java">import java.util.HashMap;public class LFUCache {static class Node{int key, value, freq = 1;Node prev, next;Node(int key, int value){this.key = key;this.value = value;}}// 键到节点的映射private HashMap<Integer, Node> keyToNode = new HashMap<>();// 频率到虚拟头节点的映射private HashMap<Integer, Node> freqToDummy = new HashMap<>();// 最小访问频率private int minFreq;// 容量private int capacity;public LFUCache(int capacity) {this.capacity = capacity;}private Node newList() {Node dummy = new Node(0, 0);dummy.prev = dummy;dummy.next = dummy;return dummy;}/*** 根据键获取值。* 该方法首先尝试从keyToNode映射中获取给定键对应的节点。如果节点不存在,说明该键不存在于数据结构中,方法将返回-1。* 如果节点存在,则该方法会执行删除操作(del)和频率更改操作(changeFreq),然后再返回节点的值。* 这种设计可能是为了在获取值的同时,根据获取情况动态调整数据结构,例如实现一种基于频率的缓存淘汰策略。** @param key 需要获取值的键。* @return 键对应的值,如果键不存在,则返回-1。*/public int get(int key) {// 尝试从映射中获取给定键对应的节点。Node node = keyToNode.get(key);// 如果节点不存在,说明键不存在于数据结构中,返回-1。if(node == null) return -1;// 修改节点的频率信息,changeFreq(node);// 返回节点的值。return node.value;}/*** 插入一个键值对到缓存中。* 如果键已经存在,则更新其值,并根据更新后的频率进行调整。* 如果缓存已满,则移除最低频率的键值对,并插入新的键值对。** @param key 插入或更新的键。* @param value 插入或更新的值。*/public void put(int key, int value) {// 尝试从映射中获取现有的节点Node node = keyToNode.get(key);if(node != null){// 如果节点存在,更新其值,并准备进行频率更新操作node.value = value;changeFreq(node);return;}// 如果缓存已满,需要移除最低频率的节点以腾出空间if(keyToNode.size() == capacity){Node cur = freqToDummy.get(minFreq);Node delNode = cur.prev;keyToNode.remove(delNode.key);del(delNode);if(cur.prev == cur) freqToDummy.remove(minFreq);}// 创建新节点,并插入到映射和频率链表中node = new Node(key, value);keyToNode.put(key, node);insert(1, node);minFreq = 1;}/*** 修改节点的频率。* 此方法用于更新给定节点的频率,并相应地调整频率列表和最小频率的值。* 如果更新后的频率导致原有的频率列表头部节点成为一个孤立节点(即它的前向和后向指针都指向自己),* 则该节点将从频率列表中移除,并且如果该频率原本是最低频率,则最小频率值将增加。* 最后,使用更新后的频率将节点插入到频率列表的适当位置。** @param node 需要更新频率的节点。*/void changeFreq(Node node){// 1、先删除节点del(node);// 根据当前节点的频率获取频率链表的头部节点Node cur = freqToDummy.get(node.freq);// 检查当前频率的链表是否为空(即头部节点的前向指针是否指向自己)if(cur.prev == cur){// 如果链表为空,则从映射中移除该频率,并检查是否需要调整最小频率值freqToDummy.remove(node.freq);if(minFreq == node.freq){minFreq++;}}// 3、插入到新位置insert(++node.freq, node);}/*** 将给定节点插入到特定频率链表中。* 此函数假设频率对应的链表已经存在,或者如果不存在,则会创建一个新的链表。* 插入操作在链表的头部进行,以保持节点按频率排序。** @param key 节点的键值,用于确定节点应插入到哪个频率链表。* @param node 要插入的节点。*/void insert(int key, Node node){// 根据频率获取或创建对应的频率链表的头节点。// 获取频率的头节点Node cur = freqToDummy.computeIfAbsent(key, k -> newList());node.prev = cur;node.next = cur.next;cur.next.prev = node;cur.next = node;}/*** 从双向链表中删除给定节点。* 此函数不返回任何值,因为它操作的是链表的内部结构。* 它接受一个参数 node,该参数是需要被删除的节点。** @param node 需要被删除的节点。*/void del(Node node){/* 将给定节点的前一个节点的next指针指向给定节点的后一个节点,从而在链表中向前断开给定节点。 */node.next.prev = node.prev;/* 将给定节点的后一个节点的prev指针指向给定节点的前一个节点,从而在链表中向后断开给定节点。 */node.prev.next = node.next;}}

http://www.ppmy.cn/ops/53172.html

相关文章

如何给小语种视频生成字幕

目前我们常看的有视频有中、英、日、韩这四种语言&#xff0c;如果我们想给其他的不常用的语言生成字幕怎么办&#xff1f;今天教大家如何给其他语言生成视频字幕文件 打开智游剪辑&#xff08;zyjj.cc&#xff09;搜索字幕生成&#xff0c;选择多语种那个就可以了 然后上传我们…

matlab用spdiags 函数构造大型稀疏对角矩阵

在Matlab中&#xff0c;spdiags函数主要用于创建稀疏对角矩阵或修改现有的稀疏矩阵的对角线。它的语法如下&#xff1a; B spdiags(B, d, m, n)其中各个参数的含义如下&#xff1a; B&#xff1a;可以是一个向量或者一个矩阵&#xff0c;用来表示对角线的值。如果B是向量&…

neo4j端口号不能访问的问题

安装可能出现的问题 访问Neo4j验证失败&#xff08;The client is unauthorized due to authentication failure.&#xff09;大概意思就是说服务器验证失败。 如果你有在浏览器上登录不同的neo4j数据库&#xff0c;很可能是由于缓存没有清理掉导致的。 可以试试无痕浏览来访问…

Linux——数据流和重定向,制作镜像

1. 数据流 标准输入&#xff08; standard input &#xff0c;简称 stdin &#xff09;&#xff1a;默认情况下&#xff0c;标准输入指从键盘获取的输入 标准输出&#xff08; standard output &#xff0c;简称 stdout &#xff09;&#xff1a;默认情况下&#xff0c;命令…

python循环结构

1.while 循环 语句&#xff1a; while 循环条件表达式&#xff1a; 代码块 else&#xff1a; 代码块 小练&#xff1a; 设计一百以内的偶数相加 n 0 while n < 100:n 1if n % 2 0 :print(n) 判断是不是闰年&#xff08;四年一润和百年不润&#xff0c;或者四百年一润&am…

C++观察者模式

一、定义 观察者&#xff08;Observer&#xff09;模式 定义如下&#xff1a;是一种对象间的一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。 二、观察者模式组成&#xff1a; 抽象目标角色&#xff08…

Redis 内存碎片是什么?如何清理?

Redis 内存碎片相关的问题在得物、美团、阿里、字节、携程等公司的后端面试中都曾出现过&#xff0c;还是建议认真准备一下。即使不是准备面试&#xff0c;日常开发也是能够用到的&#xff01; 什么是内存碎片? 你可以将内存碎片简单地理解为那些不可用的空闲内存。 举个例子&…

Java学习:第九章接口

★ 抽象方法和抽象类 ★ 接口 ★ 策略设计模式和适配器设计模式 抽象类和抽象方法&#xff1a; 1、 抽象方法 ★ 使用关键字 abstract修饰的方法称为抽象方法, 仅有方法声明没有方法体。 2、 抽象类 ★包含抽象方法的类称为抽象类&#xff0c;必须也用abstrac…