LFU 的设计与实现

article/2025/10/16 22:39:33

LFU 的设计与实现

作者:Grey

原文地址:

博客园:LFU 的设计与实现

CSDN:LFU 的设计与实现

题目描述

LFU(least frequently used)。即最不经常使用页置换算法。

题目链接:LeetCode 460. LFU Cache

主要思路

首先,定义一个辅助数据结构 Node

    public static class Node {public Integer key;public Integer value;public Integer times; // 这个节点发生get或者set的次数总和public Node up; // 节点之间是双向链表所以有上一个节点public Node down; // 节点之间是双向链表所以有下一个节点public Node(int k, int v, int t) {key = k;value = v;times = t;}}

这个 Node 用于封装 LFU Cache 每次加入的元素,其中 key 和 value 两个变量记录每次加入的 KV 值,times 用于记录该 KV 值被操作(get/set)的次数之和, up 和 down 两个变量用于链接和 KV 出现词频一样的数据项,用链表串联。

接下来需要另外一个辅助数据结构 NodeList,前面的 Node 结构已经把词频一致的数据项组织在同一个桶里,这个 NodeList 用于连接出现不同词频的桶,用双向链表组织

    public static class NodeList {public Node head; // 桶的头节点public Node tail; // 桶的尾节点public NodeList last; // 桶之间是双向链表所以有前一个桶public NodeList next; // 桶之间是双向链表所以有后一个桶public NodeList(Node node) {head = node;tail = node;}……}

使用一个具体的示例来表示上述两个结构如何组织的

例如,LFU Cache 在初始为空的状态下,进来如下数据

key = A, value = 3

key = B, value = 30

key = C, value = 4

key = D, value = 12

那么 LFU 会做如下组织

img
此时只有出现一次的桶,接下来,如果 key = C 这条记录 被访问过了,所以词频变为2,接下来要把 key = C 这条记录先从词频为1的桶里面取出来,然后再新建一个词频为 2 的桶,把这个 key = C 的数据项挂上去,结果如下

img

接下来,如果又操作了 key = C 这条记录,那么这条记录的词频就是 3, 又需要新增一个词频为 3 的桶,原来词频为 2 的桶已经没有数据项了,要销毁,并且把词频为 1 的桶和词频为 3 的桶连接在一起。

img

接下来,如果操作了 key = A,则 key = A 成为词频为 2 的数据项,再次新增词频为 2 的桶,并把这个桶插入到词频为 1 和词频为 3 的桶之间,如下图

img

以上示例就可以很清楚说明了 Node 和 NodeList 两个数据结构在 LFU 中的作用,接下来,为了实现快速的 put 和 get 操作,需要定义如下成员变量

int capacity; // 缓存的大小限制
int size; // 缓存目前有多少个节点
HashMap<Integer, Node> records; // 表示key(Integer)由哪个节点(Node)代表
HashMap<Node, NodeList> heads; // 表示节点(Node)在哪个桶(NodeList)里
NodeList headList; // 整个结构中位于最左的桶,是一个双向链表

说明:records 这个变量就是用于快速得到某个 key 的节点(Node)是什么,由于这里的 kv 都是整型,所以用 Integer 作为 key 可以定位到对应的 Node 数据项信息。

heads 则用于快速定位某个 Node 在哪个桶里面。

headList 表示整个结构中位于最左侧的桶,这个桶一定是出现次数最少的桶,所以淘汰的时候,优先淘汰这个桶里面的末尾位置,即 tail 位置的 node!

两个核心方法 put 和 get 的核心代码说明如下

    public void put(int key, int value) {if (records.containsKey(key)) {
// put 的元素是已经存在的
// 更新元素值,更新出现次数Node node = records.get(key);node.value = value;node.times++;// 通过heads以O(1)复杂度定位到所在的桶NodeList curNodeList = heads.get(node);// 把这个更新后的 Node 从 旧的桶迁移到新的桶move(node, curNodeList);} else {if (size == capacity) {// 容量已经满了// 淘汰 headList 尾部的节点!因为这个节点是最久且最少用过的节点Node node = headList.tail;headList.deleteNode(node);// 删掉的节点有可能会让 headList 换头,因为最右侧的桶可能只有一个节点,被删除后,就没有了。modifyHeadList(headList);// records和 heads 中都要删掉其记录records.remove(node.key);heads.remove(node);size--;}// 以上操作就是淘汰了一个节点// 接下来就放心加入节点// 先建立Node,词频设置为 1Node node = new Node(key, value, 1);if (headList == null) {// 如果headList为空,说明最左侧的桶没有了,新来节点正好充当最左侧节点的桶中元素headList = new NodeList(node);} else {if (headList.head.times.equals(node.times)) {// 最右侧桶不为空的情况下,这个节点出现的次数又正好等于最左侧桶所代表的节点数// 则直接加入最左侧桶中headList.addNodeFromHead(node);} else {// 将加入的节点作为做左侧桶,接上原先的headList// eg:新加入的节点出现的次数是1,原先的 headList代表的桶是词频为2的数据// 就会走这个分支NodeList newList = new NodeList(node);newList.next = headList;headList.last = newList;headList = newList;}}records.put(key, node);heads.put(node, headList);size++;}}public int get(int key) {if (!records.containsKey(key)) {// 不包含这个key// 按题目要求直接返回 -1return -1;}// 否则,先取出这个节点Node node = records.get(key);// 词频+1node.times++;// 将这个节点所在的桶找到NodeList curNodeList = heads.get(node);// 将这个节点从原桶调整到新桶move(node, curNodeList);return node.value;}

PS:这里涉及的对双向链表和桶链表的两个操作movemodifyHeadList逻辑不难,但是很多繁琐的边界条件要处理,具体方法的说明见上述代码注释,不赘述。

完整代码如下

static class LFUCache {private int capacity; // 缓存的大小限制private int size; // 缓存目前有多少个节点private HashMap<Integer, Node> records; // 表示key(Integer)由哪个节点(Node)代表private HashMap<Node, NodeList> heads; // 表示节点(Node)在哪个桶(NodeList)里private NodeList headList; // 整个结构中位于最左的桶public LFUCache(int capacity) {this.capacity = capacity;size = 0;records = new HashMap<>();heads = new HashMap<>();headList = null;}// 节点的数据结构public static class Node {public Integer key;public Integer value;public Integer times; // 这个节点发生get或者set的次数总和public Node up; // 节点之间是双向链表所以有上一个节点public Node down; // 节点之间是双向链表所以有下一个节点public Node(int k, int v, int t) {key = k;value = v;times = t;}}// 桶结构public static class NodeList {public Node head; // 桶的头节点public Node tail; // 桶的尾节点public NodeList last; // 桶之间是双向链表所以有前一个桶public NodeList next; // 桶之间是双向链表所以有后一个桶public NodeList(Node node) {head = node;tail = node;}// 把一个新的节点加入这个桶,新的节点都放在顶端变成新的头部public void addNodeFromHead(Node newHead) {newHead.down = head;head.up = newHead;head = newHead;}// 判断这个桶是不是空的public boolean isEmpty() {return head == null;}// 删除node节点并保证node的上下环境重新连接public void deleteNode(Node node) {if (head == tail) {head = null;tail = null;} else {if (node == head) {head = node.down;head.up = null;} else if (node == tail) {tail = node.up;tail.down = null;} else {node.up.down = node.down;node.down.up = node.up;}}node.up = null;node.down = null;}}private boolean modifyHeadList(NodeList removeNodeList) {if (removeNodeList.isEmpty()) {if (headList == removeNodeList) {headList = removeNodeList.next;if (headList != null) {headList.last = null;}} else {removeNodeList.last.next = removeNodeList.next;if (removeNodeList.next != null) {removeNodeList.next.last = removeNodeList.last;}}return true;}return false;}private void move(Node node, NodeList oldNodeList) {oldNodeList.deleteNode(node);NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.last : oldNodeList;NodeList nextList = oldNodeList.next;if (nextList == null) {NodeList newList = new NodeList(node);if (preList != null) {preList.next = newList;}newList.last = preList;if (headList == null) {headList = newList;}heads.put(node, newList);} else {if (nextList.head.times.equals(node.times)) {nextList.addNodeFromHead(node);heads.put(node, nextList);} else {NodeList newList = new NodeList(node);if (preList != null) {preList.next = newList;}newList.last = preList;newList.next = nextList;nextList.last = newList;if (headList == nextList) {headList = newList;}heads.put(node, newList);}}}public void put(int key, int value) {if (capacity == 0) {return;}if (records.containsKey(key)) {Node node = records.get(key);node.value = value;node.times++;NodeList curNodeList = heads.get(node);move(node, curNodeList);} else {if (size == capacity) {Node node = headList.tail;headList.deleteNode(node);modifyHeadList(headList);records.remove(node.key);heads.remove(node);size--;}Node node = new Node(key, value, 1);if (headList == null) {headList = new NodeList(node);} else {if (headList.head.times.equals(node.times)) {headList.addNodeFromHead(node);} else {NodeList newList = new NodeList(node);newList.next = headList;headList.last = newList;headList = newList;}}records.put(key, node);heads.put(node, headList);size++;}}public int get(int key) {if (!records.containsKey(key)) {return -1;}Node node = records.get(key);node.times++;NodeList curNodeList = heads.get(node);move(node, curNodeList);return node.value;}}

更多

算法和数据结构笔记

参考资料

算法和数据结构体系班-左程云


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

相关文章

Redis的LRU和LFU浅谈

1.概论 Redis中的缓存淘汰算法大体分为两种&#xff0c;volatile-xxx和allkeys-xxx。volatile-xxx 是对带过期时间的 key 进行淘汰&#xff0c;即使用了expire的key&#xff1b;allkeys-xxx 策略会对所有的key 进行淘汰。如果只是用redis做缓存&#xff0c;几乎不使用持久…

LFU五种实现方式,从简单到复杂

前言 最近刷力扣题&#xff0c;对于我这种 0 基础来说&#xff0c;真的是脑壳疼啊。这个月我估计都是中等和困难题&#xff0c;没有简单题了。 幸好&#xff0c;力扣上有各种大牛给写题解。看着他们行云流水的代码&#xff0c;真的是羡慕不已。让我印象最深刻的就是人称 “甜…

LRU和LFU

LRU 根据题意&#xff0c;使用过的数据放在链表的尾部&#xff0c;容量满了就删除链表的头&#xff0c;使用的数据结构是LinkedHashMap。 146. LRU 缓存(中等) class LRUCache {private int cap;private LinkedHashMap<Integer, Integer> list;public LRUCache(int…

LFU缓存详解

LFU缓存详解 文章目录 LFU缓存详解一、LFU概念二、分析方法一&#xff1a;哈希表 平衡二叉树方法二&#xff1a;双哈希表 一、LFU概念 LRU详解&#xff1a;https://blog.csdn.net/wolfGuiDao/article/details/105862106 LRU 算法相当于把数据按照时间排序&#xff0c;这个需求…

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;点击即可前往文青乐…