LFU算法详解

article/2025/10/17 0:59:14

LFU算法:least frequently used,最近最不经常使用算法

对于每个条目,维护其使用次数 cnt最近使用时间 time

cache容量为 n,即最多存储n个条目。

那么当我需要插入新条目并且cache已经满了的时候,需要删除一个之前的条目。删除的策略是:优先删除使用次数cnt最小的那个条目,因为它最近最不经常使用,所以删除它。如果使用次数cnt最小值为min_cnt,这个min_cnt对应的条目有多个,那么在这些条目中删除最近使用时间time最早的那个条目(举个栗子:a资源和b资源都使用了两次,但a资源在5s的时候最后一次使用,b资源在7s的时候最后一次使用,那么删除a,因为b资源更晚被使用,所以b资源相比a资源来说,更有理由继续被使用,即时间局部性原理)。

类似lru算法的想法,利用哈希表+链表

链表是负责按时间先后排序的。哈希表是负责O(1)时间查找key对应节点的。

引用力扣的一张图片

 lfu算法是按照两个维度引用计数最近使用时间来排序的。所以一个链表肯定不够用了。解决办法就是按照下图这样,使用第二个哈希表,key是引用计数,value是一个链表,存储使用次数为当前key的所有节点。该链表中的所有节点按照最近使用时间排序,最近使用的在链表头部,最晚使用的在尾部。这样我们可以完成O(1)时间查找key对应节点(通过第一个哈希表);O(1)时间删除、更改某节点(通过第二个哈希表)。

注意:get(查询)操作和put(插入)操作都算“使用”,都会增加引用计数。

所以get(key)操作实现思路:如果第一个哈希表中能查到key,那么取得相应链表节点。接下来在第二个哈希表中,把它移到其引用计数+1位置的链表头部,并删除之前的节点

put(key,value)操作实现思路:如果第一个哈希表中能查找key,那么操作和get(key)一样,只是把新节点的value置为新value。

如果查不到key,那么我们有可能需要删除cache中的某一项(容量已经达到限制):直接找到第二个哈希表中最小引用计数的链表,删除其末尾节点(最晚使用),之后再添加新节点即可

注意点:

1.容量超限需要删除节点时,删除了第二个哈希表中的项的同时,第一个哈希表中对应的映射也应该删掉。

2.需要保持一个min_cnt整型变量用来保存当前的最小引用计数因为容量超限需要删除节点时,我们需要O(1)时间找到需要删除的节点。

实现代码:
 

import java.util.HashMap;
import java.util.Map;public class LFUCache {/*** key 就是题目中的 key* value 是结点类*/private Map<Integer, ListNode> map;/*** 访问次数哈希表,使用 ListNode[] 也可以,不过要占用很多空间*/private Map<Integer, DoubleLinkedList> frequentMap;/*** 外部传入的容量大小*/private Integer capacity;/*** 全局最高访问次数,删除最少使用访问次数的结点时会用到*/private Integer minFrequent = 1;public LFUCache(int capacity) {// 显式设置哈希表的长度 = capacity 和加载因子 = 1 是为了防止哈希表扩容带来的性能消耗// 这一步操作在理论上的可行之处待讨论,实验下来效果是比较好的map = new HashMap<>(capacity, 1);frequentMap = new HashMap<>();this.capacity = capacity;}/*** get 一次操作,访问次数就增加 1;* 从原来的链表调整到访问次数更高的链表的表头** @param key* @return*/public int get(int key) {// 测试测出来的,capacity 可能传 0if (capacity == 0) {return -1;}if (map.containsKey(key)) {// 获得结点类ListNode listNode = removeListNode(key);// 挂接到新的访问次数的双向链表的头部int frequent = listNode.frequent;addListNode2Head(frequent, listNode);return listNode.value;} else {return -1;}}/*** @param key* @param value*/public void put(int key, int value) {if (capacity == 0) {return;}// 如果 key 存在,就更新访问次数 + 1,更新值if (map.containsKey(key)) {ListNode listNode = removeListNode(key);// 更新 valuelistNode.value = value;int frequent = listNode.frequent;addListNode2Head(frequent, listNode);return;}// 如果 key 不存在// 1、如果满了,先删除访问次数最小的的末尾结点,再删除 map 里对应的 keyif (map.size() == capacity) {// 1、从双链表里删除结点DoubleLinkedList doubleLinkedList = frequentMap.get(minFrequent);ListNode removeNode = doubleLinkedList.removeTail();// 2、删除 map 里对应的 keymap.remove(removeNode.key);}// 2、再创建新结点放在访问次数为 1 的双向链表的前面ListNode newListNode = new ListNode(key, value);addListNode2Head(1, newListNode);map.put(key, newListNode);// 【注意】因为这个结点是刚刚创建的,最少访问次数一定为 1this.minFrequent = 1;}// 以下部分主要是结点类和双向链表的操作/*** 结点类,是双向链表的组成部分*/private class ListNode {private int key;private int value;private int frequent = 1;private ListNode pre;private ListNode next;public ListNode() {}public ListNode(int key, int value) {this.key = key;this.value = value;}}/*** 双向链表*/private class DoubleLinkedList {/*** 虚拟头结点,它无前驱结点*/private ListNode dummyHead;/*** 虚拟尾结点,它无后继结点*/private ListNode dummyTail;/*** 当前双向链表的有效结点数*/private int count;public DoubleLinkedList() {// 虚拟头尾结点赋值多少无所谓this.dummyHead = new ListNode(-1, -1);this.dummyTail = new ListNode(-2, -2);dummyHead.next = dummyTail;dummyTail.pre = dummyHead;count = 0;}/*** 把一个结点类添加到双向链表的开头(头部是最新使用数据)** @param addNode*/public void addNode2Head(ListNode addNode) {ListNode oldHead = dummyHead.next;// 两侧结点指向它dummyHead.next = addNode;oldHead.pre = addNode;// 它的前驱和后继指向两侧结点addNode.pre = dummyHead;addNode.next = oldHead;count++;}/*** 把双向链表的末尾结点删除(尾部是最旧的数据,在缓存满的时候淘汰)** @return*/public ListNode removeTail() {ListNode oldTail = dummyTail.pre;ListNode newTail = oldTail.pre;// 两侧结点建立连接newTail.next = dummyTail;dummyTail.pre = newTail;// 它的两个属性切断连接oldTail.pre = null;oldTail.next = null;// 重要:删除一个结点,当前双向链表的结点个数少 1count--;// 维护return oldTail;}}/*** 将原来访问次数的结点,从双向链表里脱离出来** @param key* @return*/private ListNode removeListNode(int key) {// 获得结点类ListNode deleteNode = map.get(key);ListNode preNode = deleteNode.pre;ListNode nextNode = deleteNode.next;// 两侧结点建立连接preNode.next = nextNode;nextNode.pre = preNode;// 删除去原来两侧结点的连接deleteNode.pre = null;deleteNode.next = null;// 维护双链表结点数frequentMap.get(deleteNode.frequent).count--;// 【注意】维护 minFrequent// 如果当前结点正好在最小访问次数的链表上,并且移除以后结点数为 0,最小访问次数需要加 1if (deleteNode.frequent == minFrequent && frequentMap.get(deleteNode.frequent).count == 0) {// 这一步需要仔细想一下,经过测试是正确的minFrequent++;}// 访问次数加 1deleteNode.frequent++;return deleteNode;}/*** 把结点放在对应访问次数的双向链表的头部** @param frequent* @param addNode*/private void addListNode2Head(int frequent, ListNode addNode) {DoubleLinkedList doubleLinkedList;// 如果不存在,就初始化if (frequentMap.containsKey(frequent)) {doubleLinkedList = frequentMap.get(frequent);} else {doubleLinkedList = new DoubleLinkedList();}// 添加到 DoubleLinkedList 的表头doubleLinkedList.addNode2Head(addNode);frequentMap.put(frequent, doubleLinkedList);}public static void main(String[] args) {LFUCache cache = new LFUCache(2);cache.put(1, 1);cache.put(2, 2);System.out.println(cache.map.keySet());int res1 = cache.get(1);System.out.println(res1);cache.put(3, 3);System.out.println(cache.map.keySet());int res2 = cache.get(2);System.out.println(res2);int res3 = cache.get(3);System.out.println(res3);cache.put(4, 4);System.out.println(cache.map.keySet());int res4 = cache.get(1);System.out.println(res4);int res5 = cache.get(3);System.out.println(res5);int res6 = cache.get(4);System.out.println(res6);}
}作者:liweiwei1419
链接:https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-biao-shuang-xiang-lian-biao-java-by-liweiwei/
来源:力扣(LeetCode)

参考链接:LFU算法实现(460. LFU缓存) - NeoZy - 博客园


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

相关文章

算法题就像搭乐高:手把手带你拆解 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;所以我就结合自己的亲身体会来…

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的选择…