手撕 LFU 缓存

大家好,我是 方圆。LFU 的缩写是 Least Frequently Used,简单理解则是将使用最少的元素移除,如果存在多个使用次数最小的元素,那么则需要移除最近不被使用的元素。LFU 缓存在 LeetCode 上是一道困难的题目,实现起来并不容易,所以决定整理和记录一下。如果大家想要找刷题路线的话,可以参考 Github: LeetCode。

LFU 缓存

LeetCode 原题:460. LFU 缓存 困难。

思路:我们需要维护两个 HashMap,分别为 keyNodeMapaccessNodeMap,它们的职责如下:

  • keyNodeMap: key 为 put 的值的 key 值,value 为该 key 对应的节点,那么我们便可以通过这个 map 以 O(1) 的时间复杂度 get 到对应的值

  • accessNodeMap: key 为访问次数,各个节点被 put 和 get 都会使节点 accessNum 访问次数加 1,value 为该访问次数下的 循环双向链表的头节点,通过双向链表我们能以 O(1) 的时间复杂度将节点移除。我们定义 在相同访问次数下,越早插入的节点越靠近双向链表的尾端,在进行节点移除时,会将尾节点移除。

为了更好的理解两个 map 与链表节点的关系,我们用下图对容量为 3 的缓存进行表示,其中绿色代表链表节点,节点中各个值对应的字段为 key, value, accessNum

lfu.png

除此之外,我们要定义一个 minAccessNum 的字段来维护当前缓存中最小的访问次数,这样我们就能够在时间复杂度为 O(1) 的情况下在 accessNodeMap 中获取到对应访问次数的双向链表。

大致的方向确定了,我们需要再想一下具体的实现:

get 方法:我们首先去 keyNodeMap 中拿,没有的话返回 -1 即可。如果有对应的 key 的话,那么我们需要将对应节点的访问次数加 1,并需要改变它所在 accessNodeMap 中的位置:首先需要断开它与原链表的连接,之后加入到新的链表中,如果在 accessNodeMap 中有对应次数的链表,那么我们需要把它插入到该链表的 头节点;如果没有对应访问次数的双向链表的话,我们需要创建该访问次数的链表,并以该节点为头节点,维护在 accessNodeMap 中。这里需要注意,我们要对 minAccessNum 进行 更新,如果该节点的访问次数和 minAccessNum 相等,并且该节点在原来链表删除后,该访问次数下的链表中不存在其他任何节点,那么 minAccessNum 也要加 1。

put 方法:我们同样也需要在 keyNodeMap 中判断是否存在,存在的话需要将值进行覆盖,之后的处理逻辑与 get 方法一致。如果不存在的话,我们这里需要判断缓存的容量 是否足够,足够的话比较简单,先将其 put 到 keyNodeMap 中,再在 accessNodeMap 中将其插入到 key 为 1 的双向链表的头节点即可,这里要注意更改 minAccessNum 为 1,因为新插入的节点一定是访问次数最少的;如果不够的话那么先要 将最少使用的节点移除(在两个 map 中都要移除),在 accessNodeMap 中进行移除时,需要根据 minAccessNum 获取对应的双向链表,移除它的尾节点。在尾节点移除完之后,执行的逻辑和上述容量足够时执行插入节点的逻辑一致。

具体实现已经比较清楚了,直接上代码吧,大家可以关注一下注释信息:

class LFUCache {/*** 双向链表节点*/static class Node {Node left;Node right;int key;int value;int accessNum;public Node(int key, int value, int accessNum) {this.key = key;this.value = value;this.accessNum = accessNum;}}private HashMap<Integer, Node> keyNodeMap;private HashMap<Integer, Node> accessNodeMap;private int minAccessNum;private int capacity;public LFUCache(int capacity) {keyNodeMap = new HashMap<>(capacity);accessNodeMap = new HashMap<>(capacity);minAccessNum = 0;this.capacity = capacity;}public int get(int key) {if (keyNodeMap.containsKey(key)) {Node node = keyNodeMap.get(key);// 如果所在链表只有一个节点的话,那么直接将该访问次数的链表删掉if (node == node.right) {accessNodeMap.remove(node.accessNum);// 维护缓存中最小的访问次数if (minAccessNum == node.accessNum) {minAccessNum++;}} else {// 断开与原链表的连接node.left.right = node.right;node.right.left = node.left;// 如果该节点是头节点的话,那么需要替换为它的下一个节点作为头节点if (node == accessNodeMap.get(node.accessNum)) {accessNodeMap.put(node.accessNum, node.right);}}// 增加后的访问次数链表看看有没有node.accessNum++;if (accessNodeMap.containsKey(node.accessNum)) {Node target = accessNodeMap.get(node.accessNum);// 插入头节点insertHead(node, target);} else {// 没有的话,直接 put 即可accessNodeMap.put(node.accessNum, node);// 单节点循环链表node.left = node;node.right = node;}return node.value;} else {return -1;}}public void put(int key, int value) {if (keyNodeMap.containsKey(key)) {Node node = keyNodeMap.get(key);node.value = value;// 执行get方法get(key);} else {Node node = new Node(key, value, 1);if (keyNodeMap.size() == capacity) {// 容量不够需要将最少使用的节点移除Node oldNodeHead = accessNodeMap.get(minAccessNum);Node tail = oldNodeHead.left;// 如果所在链表只有一个节点的话,那么直接将该访问次数的链表删掉if (tail.right == tail) {accessNodeMap.remove(tail.accessNum);} else {// 断开与原链表的连接tail.left.right = tail.right;tail.right.left = tail.left;// 如果该节点是头节点的话,那么需要替换为它的下一个节点作为头节点if (oldNodeHead == accessNodeMap.get(tail.accessNum)) {accessNodeMap.put(tail.accessNum, tail.right);}}keyNodeMap.remove(tail.key);}// 这样就有有足够的容量了keyNodeMap.put(key, node);// 是否有对应的链表if (accessNodeMap.containsKey(node.accessNum)) {// 插入头节点insertHead(node, accessNodeMap.get(node.accessNum));} else {// 没有对应的链表 直接插入accessNodeMap.put(node.accessNum, node);node.left = node;node.right = node;}minAccessNum = 1;}}private void insertHead(Node node, Node target) {// 拼接到该链表头,并构建循环双向链表node.right = target;node.left = target.left;target.left.right = node;target.left = node;// 覆盖头节点accessNodeMap.put(node.accessNum, node);}}

需要注意的是:

  1. 因为我们维护的是循环双向链表,所以在插入头节点时注意尾节点和头节点的引用关系

  2. 因为我们在 accessNodeMap 中维护的是头节点,所以当我们将链表的头结点进行移除时,需要将头节点的下一个节点作为新的头节点保存在 accessNodeMap

针对第二点我们可以做一个优化,每当第一次生成双向链表的时候,我们创建一个哨兵节点作为头节点,那么这样我们就无需在头节点被移除后再将新的头节点插入 accessNodeMap 中进行覆盖了,始终保持 accessNodeMap 中 value 值保存的是哨兵节点,最终代码如下:

class LFUCache {/*** 双向链表节点*/static class Node {int key, value;Node pre, next;int accessNum;public Node(int key, int value, int accessNum) {this.key = key;this.value = value;this.accessNum = accessNum;}}/*** 记录访问最小的值*/private int minAccessNum;private final int capacity;private final HashMap<Integer, Node> accessNodeMap;private final HashMap<Integer, Node> keyNodeMap;public LFUCache(int capacity) {this.capacity = capacity;accessNodeMap = new HashMap<>(capacity);keyNodeMap = new HashMap<>(capacity);// 初始化访问次数为 1 的哨兵节点minAccessNum = 1;accessNodeMap.put(minAccessNum, initialSentinelNode(minAccessNum));}public int get(int key) {if (keyNodeMap.containsKey(key)) {Node node = keyNodeMap.get(key);// 找到新的位置insertIntoNextSentinel(node);return node.value;}return -1;}public void put(int key, int value) {if (keyNodeMap.containsKey(key)) {Node node = keyNodeMap.get(key);node.value = value;insertIntoNextSentinel(node);} else {if (keyNodeMap.size() == capacity) {// 移除最老的节点removeEldest();}// 新加进来的肯定是最小访问次数 1minAccessNum = 1;Node newNode = new Node(key, value, minAccessNum);// 插入到头节点insertIntoHead(newNode, accessNodeMap.get(minAccessNum));keyNodeMap.put(key, newNode);}}/*** 插入下一个链表中*/private void insertIntoNextSentinel(Node node) {// 在原来的位置移除remove(node);// 尝试更新 minAccessNumtryToIncreaseMinAccessNum(node.accessNum++);// 获取增加 1 后的哨兵节点Node nextCacheSentinel = getSpecificAccessNumSentinel(node.accessNum);// 插入该链表的头节点insertIntoHead(node, nextCacheSentinel);}/*** 在原链表中移除*/private void remove(Node node) {node.pre.next = node.next;node.next.pre = node.pre;node.next = null;node.pre = null;}/*** 尝试更新 minAccessNum*/private void tryToIncreaseMinAccessNum(int accessNum) {// 原访问次数的哨兵节点Node originSentinel = accessNodeMap.get(accessNum);// 如果只剩下哨兵节点的话,需要看看需不需要把 minAccessNum 增加 1if (originSentinel.next == originSentinel && originSentinel.accessNum == minAccessNum) {minAccessNum++;}}/*** 获取指定访问次数的哨兵节点*/private Node getSpecificAccessNumSentinel(int accessNum) {if (accessNodeMap.containsKey(accessNum)) {return accessNodeMap.get(accessNum);} else {// 没有的话得初始化一个Node nextCacheSentinel = initialSentinelNode(accessNum);accessNodeMap.put(accessNum, nextCacheSentinel);return nextCacheSentinel;}}/*** 生成具体访问次数的哨兵节点*/private Node initialSentinelNode(int accessNum) {Node sentinel = new Node(-1, -1, accessNum);// 双向循环链表sentinel.next = sentinel;sentinel.pre = sentinel;return sentinel;}/*** 插入头节点*/private void insertIntoHead(Node node, Node nextCacheSentinel) {node.next = nextCacheSentinel.next;nextCacheSentinel.next.pre = node;nextCacheSentinel.next = node;node.pre = nextCacheSentinel;}/*** 如果容量满了的话,需要把 minAccessNum 访问次数的尾巴节点先移除掉*/private void removeEldest() {Node minSentinel = accessNodeMap.get(minAccessNum);Node tail = minSentinel.pre;tail.pre.next = tail.next;minSentinel.pre = tail.pre;keyNodeMap.remove(tail.key);}
}

巨人的肩膀

  • LFU 缓存官方题解

  • 【宫水三叶】运用「桶排序」&「双向链表」实现 LFUCache

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/83989.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Godot】解决游戏中的孤立/孤儿节点及分析器性能问题的分析处理

Godot 4.1 因为我在游戏中发现&#xff0c;越运行游戏变得越来越卡&#xff0c;当你使用 Node 节点中的 print_orphan_nodes() 方法打印信息的时候&#xff0c;会出现如下的孤儿节点信息 孤儿节点信息是以 节点实例ID - Stray Node: 节点名称(Type: 节点类型) 作为格式输出&a…

腾讯mini项目-【指标监控服务重构】2023-08-23

今日已办 进度和问题汇总 请求合并 feature/venus tracefeature/venus metricfeature/profile-otel-baserunner-stylebugfix/profile-logger-Syncfeature/profile_otelclient_enable_config 完成otel 开关 trace-采样metrice-reader 已经都在各自服务器运行&#xff0c;并接入…

创造性地解决冲突

1、冲突的根本原因是矛盾双方存在不可调和的目标冲突。 2、要知己知彼&#xff1a; 知己&#xff1a;就是对自己的问题、需求进行客观定义&#xff0c;说明需求和问题的意义或价值、阐述解决方案和期望效果&#xff1b; 知彼&#xff1a;站在对方立场&#xff0c;深挖对方真…

根据3d框的八个顶点坐标,求他的中心点,长宽高和yaw值(Python)

要从一个3D框的八个顶点求出它的中心点、长、宽、高和yaw值&#xff0c;首先需要明确框的几何形状和坐标点的顺序。通常这样的框是一个矩形体&#xff08;长方体&#xff09;&#xff0c;但其方向并不一定与坐标轴平行。 以下是一个步骤来解决这个问题&#xff1a; 求中心点&a…

Unity Bolt UGUI事件注册方式总结

Bolt插件提供了丰富的事件注册方式&#xff0c;开发者几乎不用编写任何代码就可以完成事件的注册&#xff0c;进行交互。下面是我使用UI事件注册的相关总结。 1、通过UI控件自身拖拽实现事件的注册。 Button的事件注册&#xff1a; 新建一个UnityEvent事件&#xff0c; Butt…

Kafka消费者组重平衡(二)

文章目录 概要重平衡通知机制消费组组状态消费端重平衡流程Broker端重平衡流程 概要 上一篇Kafka消费者组重平衡主要介绍了重平衡相关的概念&#xff0c;本篇主要梳理重平衡发生的流程。 为了更好地观察&#xff0c;数据准备如下&#xff1a; kafka版本&#xff1a;kafka_2.1…

nodejs定时任务

项目需求&#xff1a; 每5秒执行一次&#xff0c;多个定时任务错开&#xff0c;即cron表达式中斜杆前带数字&#xff0c;例如 ‘1/5 * * * * *’定时任务准时&#xff0c;延误低 搜索了nodejs的定时任务&#xff0c;其实不多&#xff0c;找到了以下三个常用的&#xff1a; n…

OpenCV中的HoughLines函数和HoughLinesP函数到底有什么区别?

一、简述 基于OpenCV进行直线检测可以使用HoughLines和HoughLinesP函数完成的。这两个函数之间的唯一区别在于,第一个函数使用标准霍夫变换,第二个函数使用概率霍夫变换(因此名称为 P)。概率版本之所以如此,是因为它仅分析点的子集并估计这些点都属于同一条线的概率。此实…

2D游戏开发和3D游戏开发有什么不同?

2D游戏开发和3D游戏开发是两种不同类型的游戏制作方法&#xff0c;它们之间有一些显著的区别&#xff1a; 1. 图形和视觉效果&#xff1a; 2D游戏开发&#xff1a; 2D游戏通常使用二维图形&#xff0c;游戏世界和角色通常在一个平面上显示。这种类型的游戏具有平面的外观&…

数据仓库模型设计V2.0

一、数仓建模的意义 数据模型就是数据组织和存储方法&#xff0c;它强调从业务、数据存取和使用角度合理存储数据。只有将数据有序的组织和存储起来之后&#xff0c;数据才能得到高性能、低成本、高效率、高质量的使用。 高性能&#xff1a;良好的数据模型能够帮助我们快速查询…

shell脚本命令

Shell命令是在类Unix操作系统中使用的命令行解释器&#xff08;shell&#xff09;中执行的命令。Shell命令可以用于执行系统命令、操作文件、进行文本处理、管理进程等。以下是一些常见的Shell命令&#xff1a; 1. ls&#xff1a;列出当前目录下的文件和文件夹。 2. cd&#x…

界面组件DevExpress WinForms v23.1亮点 - 全新升级HTML CSS模板

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

2020-2023中国高等级自动驾驶产业发展趋势研究-概念界定

1.1 概念界定 自动驾驶发展过程中&#xff0c;中国出现了诸多专注于研发L3级以上自动驾驶的公司&#xff0c;其在业界地位也越来越重要。本报告围绕“高等级自动驾驶” 展开&#xff0c;并聚焦于该技术2020-2023年在中国市场的变化趋势进行研究。 1.1.1 什么是自动驾驶 自动驾驶…

C#中的方法

引言 在C#编程语言中&#xff0c;方法是一种封装了一系列可执行代码的重要构建块。通过方法&#xff0c;我们可以将代码逻辑进行模块化和复用&#xff0c;提高代码的可读性和可维护性。本文将深入探讨C#中的方法的定义、参数传递、返回值、重载、递归等方面的知识&#xff0c;…

小型水库雨水情测报和大坝安全监测解决方案

一、建设背景 我国小型水库数量众多&#xff0c;大多由农村集体经济组织管理&#xff0c;灌溉、供水、防洪、生 态效益突出&#xff0c;是农业生产、农民生活、农村发展和区域防洪的重要基础设施&#xff0c;实施乡 村振兴战略和生态文明建设的重要支撑保障。由于小型水库工程存…

zabbix自定义监控内容案例

一、自定义监控内容 案列&#xff1a;自定义监控客户端服务器登录的人数需求&#xff1a;限制登录人数不超过 3 个&#xff0c;超过 3 个就发出报警信息 1、在客户端创建自定义key 明确需要执行的linux命令 创建zabbix监控项配置文件&#xff0c;用于自定义Key #在zabbix的…

小谈设计模式(3)—策略模式

小谈设计模式&#xff08;3&#xff09;—策略模式 专栏介绍专栏地址专栏介绍 策略模式主要角色环境&#xff08;Context&#xff09;抽象策略&#xff08;Strategy&#xff09;具体策略&#xff08;Concrete Strategy&#xff09;角色总结 核心思想封装算法定义抽象策略使用环…

Selenium Grid 的搭建方法

传统 Selenium Grid 的搭建方法 搭建一个具有 1 个 Node 的 Selenium Grid。那么通常来讲我们需要 2 台机器&#xff0c;其中一台作为 Hub&#xff0c;另外一台作为 Node&#xff0c;并要求这两台机器已经具备了 Java 执行环境。 1.通过官网下载 selenium-server-standalone-…

SpringMVC之JSON数据返回异常处理机制

目录 前言 一、JSON数据返回 1.导入依赖 2.配置spring-mvc.xml 3.使用ResponseBody注解 4.Jackson 4.1.介绍 4.2.常用注解 二、异常处理机制 1.为什么要全局异常处理 2.异常处理思路 3.SpringMVC异常分类 4.综合案例 4.1.异常处理方式一 4.2.异常处理方式二 4.3…

git提示:remote origin already exists

目录 问题场景 问题原因 问题解决 问题场景 在GitLab中新建仓库后&#xff0c;然后将本地项目提交提示&#xff1a;remote origin already exists. 问题原因 error: remote origin already exists. 错误&#xff1a;远程源点已存在&#xff08;翻译&#xff09; 出现该错误的…