从 LinkedHashMap 源码到手撕 LRU 缓存

大家好,我是 方圆。最近在刷 LeetCode 上LRU缓存的题目,发现答案中有 LinkedHashMap 和自己定义双向链表的两种解法,但是我对 LinkedHashMap 相关源码并不清楚,所以准备学习和记录一下。如果大家想要找刷题路线的话,可以参考 Github: LeetCode。

LRU(Least Recently Used),即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

1. LinkedHashMap 源码

LinkedHashMap 继承了 HashMap,并使用双向链表对所有的 entry 进行管理,使得这些节点能够按照 插入顺序访问(access)顺序 来排列,并且节点的添加和移除 时间复杂度为 O(1)

顺序的模式通过字段 accessOrder 来定义,为 false 时表示插入顺序,否则为访问顺序。LinkedHashMap 中能够定义顺序模式的构造方法如下:

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder = accessOrder;
}

需要注意的是,按照插入顺序排列的 LinkedHashMap,如果 将其中已有的 key 再重新插入到 map 中,则它的节点顺序不会受到影响,我们来具体看一下源码:

LinkedHashMap 调用 put 方法时会执行 HashMap 中的 putVal 方法,关键的代码部分如下:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// ...else {// ...// map 中已经存在了这个 keyif (e != null) { V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;// 重点关注这里afterNodeAccess(e);return oldValue;}}// ...}

当 map 中已有该 key 时,会执行上述逻辑,注意其中的 afterNodeAccess 方法,它是定义在 HashMap 中的钩子方法,LinkedHashMap 对该方法做了实现,如下:

    // 将 节点 移动到末尾void afterNodeAccess(Node<K,V> e) {LinkedHashMap.Entry<K,V> last;// 需要满足是访问顺序排列和当前节点不是尾节点的条件if (accessOrder && (last = tail) != e) {// p 为当前节点,b 为 p 的前驱节点,a 为 p 的后继节点LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;// p 作为新的尾节点,after 指针为 nullp.after = null;// 处理 p 的前驱节点 b,为空的话后继节点为新的头节点if (b == null)head = a;else// 否则 b 的 after 指针指向 p 的后继节点 ab.after = a;// 处理 p 的后继节点 a,不为空的话 a 的前驱节点为 bif (a != null)a.before = b;else// 这个 else 条件与当前节点 p 不是尾节点的条件相悖,理论上 a 节点不为空last = b;// 空链表会进入到这里,将第一插入的 p 节点作为头节点if (last == null)head = p;else {// p 节点作为新的尾节点,那么它的前驱节点是原尾节点 lastp.before = last;// 原尾节点 last 的后继节点为 plast.after = p;}// tail 尾节点指针指向 ptail = p;++modCount;}}

我们可以发现在判断条件 if (accessOrder && (last = tail) != e) 中,插入顺序 accessOrder 为 false,不会执行任何逻辑,所以重新插入已有的 key 不改变节点的顺序。当 accessOrder 为 true 时,即为访问顺序时,会将该节点移动到尾节点处。

LRU 算法需要通过访问顺序来实现,所以我们需要指定 accessOrder 为 True。如果需要指定 LRU 缓存的容量(超过容量将最老的节点移除),我们需要关注 afterNodeInsertion 方法,它也是定义在 HashMap 中的钩子方法,调用时机在第一次插入节点时,关键代码如下,它在 HashMap 的 putVal 方法中:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// ...// 新节点第一次插入afterNodeInsertion(evict);return null;}

我们来关注下 LinkedHashMap 中对此方法的实现:

    // 头节点是最旧的,将头节点进行移除void afterNodeInsertion(boolean evict) { LinkedHashMap.Entry<K,V> first;// evict 为 true,且头节点不为空,removeEldestEntry 为 true 时将节点进行移除if (evict && (first = head) != null && removeEldestEntry(first)) {K key = first.key;removeNode(hash(key), key, null, false, true);}}

removeEldestEntry 方法我们需要点进去看看:

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;}

我们可以发现,该方法默认情况下为 False,所以插入节点是不会对节点进行移除的。而LRU算法需要将缓存维持在固定大小,那么我们需要对该方法进行重写,比如要保持容量大小始终在100:

private static final int MAX_ENTRIES = 100;protected boolean removeEldestEntry(Map.Entry eldest) {return size() > MAX_ENTRIES;
}

总结一下,使用 LinkedHashMap 实现 LRU 缓存需要做两件事:

  1. 调用特定的构造方法指定 accessOrder 为 true,使得每次被访问的节点都改变节点顺序

  2. 如果需要指定缓存容量的话,需重写 removeEldestEntry 方法来保证不超过指定的最大容量

2. 手撕 LRU 缓存

146. LRU 缓存 中等 是 LeetCode 要求手撕 LRU 缓存的题目,大家可以点进去看一下原题,这里我们分别做出两种解法:一种是针对上文所述的 LinkedHashMap 来实现,另一种是借助 HashMap 和我们自己使用双向链表管理 entry 来实现。

LinkedHashMap 法

该方法详细内容在上文中已有具体解释,所以这里不再赘述,直接看代码即可

class LRUCache extends LinkedHashMap<Integer, Integer> {// 指定缓存的最大容量private final int capacity;public LRUCache(int capacity) {super(capacity, 0.75F, true);this.capacity = capacity;}public int get(int key) {return super.getOrDefault(key, -1);}public void put(int key, int value) {super.put(key, value);}@Overrideprotected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {return size() > capacity;}
}

HashMap 和 双向链表

先上代码,注意其中的注释

class LRUCache {static class ListNode {ListNode left;ListNode right;int key, value;public ListNode(int key, int value) {this.key = key;this.value = value;}}private final HashMap<Integer, ListNode> map;private final ListNode sentinel;private final int capacity;/*** 定义访问过的节点移动到尾节点*/public LRUCache(int capacity) {this.map = new HashMap<>(capacity);this.capacity = capacity;// 定义单个哨兵节点形成双向循环链表来简化边界条件的判断ListNode sentinel = new ListNode(-1, -1);this.sentinel = sentinel;sentinel.right = sentinel;sentinel.left = sentinel;}public int get(int key) {if (map.containsKey(key)) {ListNode node = map.get(key);// 将该节点移动到尾节点refresh(node);return node.value;} else {return -1;}}public void put(int key, int value) {if (map.containsKey(key)) {ListNode node = map.get(key);node.value = value;// 如果已经有这个节点则需要将其移动到尾节点refresh(node);} else {ListNode node = new ListNode(key, value);// 没有的话先判断容量if (map.size() == capacity) {// 先移除头节点ListNode head = sentinel.right;map.remove(head.key);sentinel.right = head.right;head.right.left = sentinel;}// 插入到尾节点insert(node);// 管理到 map 中map.put(key, node);}}/*** 移动该节点到尾节点*/private void refresh(ListNode node) {ListNode pre = node.left, next = node.right;// 处理前驱节点 prepre.right = next;// 处理后继节点 nextnext.left = pre;ListNode tail = sentinel.left;// 将当前节点移动到尾节点tail.right = node;node.left = tail;// 构建双向循环链表node.right = sentinel;sentinel.left = node;}/*** 添加到尾节点*/private void insert(ListNode node) {ListNode tail = sentinel.left;// 添加到尾节点tail.right = node;node.left = tail;// 双向循环链表node.right = sentinel;sentinel.left = node;}
}

我们定义了一个 sentinel 哨兵节点,并让它形成一个循环的双向链表,我们可以根据该节点轻易获取到头节点(sentinel.right)和尾节点(sentinel.left)。这样做的好处是 简化了边界条件的处理,我们不需要在删除和移动链表节点的时候进行判空

链表图示如下,一个空的链表只由一个哨兵节点构成:

双向链表.png

需要注意的是,每次插入新的节点都需要注意维护循环双向链表


巨人的肩膀

  • 源于 LinkedHashMap源码

  • 【宫水三叶】设计数据结构:实现一个 LRUCache

  • 《算法导论》第 10.2 章

  • LRU_百度百科

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

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

相关文章

Redis:实现全局唯一id

&#xff08;笔记总结自《黑马点评》项目&#xff09; 一、全局ID生成器 全局ID生成器&#xff0c;是一种在分布式系统下用来生成全局唯一ID的工具&#xff0c;一般要满足下列特性&#xff1a; 二、原理 为了增加ID的安全性&#xff0c;我们可以不直接使用Redis自增的数值&…

windows下安装redis扩展库

1.根据PHP版本号&#xff0c;编译器版本号和CPU架构 选择php_redis和php_igbinary文件(如果是选择线程的情况下需要再去配置php5ts.dll) windows.php.net - /downloads/pecl/releases/redis/ windows.php.net - /downloads/pecl/releases/igbinary/ php_igbinary-3.1.2-7.2-…

Ubuntu yolov5 环境配置

查看Ubuntu版本 $ cat /proc/version Linux version 5.4.0-150-generic (builddbos03-amd64-012) (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)) #167~18.04.1-Ubuntu SMP Wed May 24 00:51:42 UTC 2023虚拟机磁盘扩容 因为在环境搭建过程中遇到了磁盘空间不足的问题&a…

【数据结构】——排序的相关习题

目录 一、选择填空判断题题型一&#xff08;插入排序——直接插入排序&#xff09;题型二&#xff08;插入排序——折半插入排序&#xff09;题型三&#xff08;插入排序——希尔排序&#xff09;题型四&#xff08;交换排序——冒泡排序&#xff09;题型五&#xff08;交换排序…

微调 TrOCR – 训练 TrOCR 识别弯曲文本

TrOCR(基于 Transformer 的光学字符识别)模型是性能最佳的 OCR 模型之一。在我们之前的文章中,我们分析了它们在单行打印和手写文本上的表现。然而,与任何其他深度学习模型一样,它们也有其局限性。TrOCR 在处理开箱即用的弯曲文本时表现不佳。本文将通过在弯曲文本数据集上…

合宙Air724UG LuatOS-Air LVGL API控件-标签 (Label)

标签 (Label) 标签是 LVGL 用来显示文字的控件。 示例代码 label lvgl.label_create(lvgl.scr_act(), nil) lvgl.label_set_recolor(label, true) lvgl.label_set_text(label, "#0000ff Re-color# #ff00ff words# #ff0000 of\n# align the lines …

golang validator 包的使用指北

看到 validator 咱们第一反应会想起啥&#xff1f;见名知意我就可以知道他是一个验证器&#xff0c;如果用过 gin web 框架的同学&#xff0c;自然是用过 gin 里面的 validator&#xff0c;只不过 gin 中使用的关键字是 binding 去做标识 开门见山 Validator 实际上是一个验证…

upload-labs文件上传漏洞通关

一、环境搭建 upload-labs是一个使用php语言编写的&#xff0c;专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。 下载地址&#xff1a;https://github.com/c0ny1/upload-labs/releases 在 win 环境下 直接解压到phpstudy下即可 二、通关 &#xff08;一&#xff09;16关…

ansible的安装和简单的块使用

目录 一、概述 二、安装 1、选择源 2、安装ansible 3、模块查看 三、实验 1、拓扑​编辑 2、设置组、ping模块 3、hostname模块 4、file模块 ​编辑 5、stat模块 6、copy模块&#xff08;本地拷贝到远程&#xff09; 7、fetch模块与copy模块类似&#xff0c;但作用…

Spring AOP使用指南: 强大的面向切面编程技术

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Spring Boot 整合 Redis,使用 RedisTemplate 客户端

文章目录 一、SpringBoot 整合 Redis1.1 整合 Redis 步骤1.1.1 添加依赖1.1.2 yml 配置文件1.1.3 Config 配置文件1.1.4 使用示例 1.2 RedisTemplate 概述1.2.1 RedisTemplate 简介1.2.2 RedisTemplate 功能 二、RedisTemplate API2.1 RedisTemplate 公共 API2.2 String 类型 A…

基于jeecg-boot的flowable流程历史记录显示修改

更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 历…

文件上传漏洞

条件竞争 条件竞争型的漏洞在很多漏洞中都有涉及&#xff0c;在文件上传中造成这种漏洞的原因是代码中是先保存上传的文件在服务器上&#xff0c;然后验证再删除的&#xff0c;这就会造成攻击者可以利用文件被保存在服务器上与被删除的时间间隙来访问文件&#xff0c;然后重新生…

基于Java+SpringBoot+Vue校园求职招聘系统的设计与实现 前后端分离【Java毕业设计·文档报告·代码讲解·安装调试】

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

OpenCV实现图像的混合

原理 这其实也是加法&#xff0c;但是不同的是两幅图像的权重不同&#xff0c;这就会给人一种混合或者透明的感觉。 图像混合的计算公式如下: g(x)(1-a)f0(x) af1(x) 通过修改α的值(0→1) &#xff0c;可以实现非常炫酷的混合。 现在我们把两幅图混合在一起。 第一幅图…

分布式多级缓存

例子&#xff08;测试环境&#xff09; 项目结构图 运行反向代理服务器也就是负责反向代理到三个nginx的nginx&#xff0c;该nignx也负责前端页面的跳转。 该nginx的conf为下: 突出位置就是该nginx需要反向代理的其他nginx的IP和端口。 Lua语法 linux安装Lua #安装lua环境 …

持安科技入选数说安全《2023中国网络安全市场年度报告》

近日&#xff0c;网络安全产业研究平台数说安全发布《2023中国网络安全市场年度报告》&#xff0c;报告共分为158页核心报告&#xff0c;及番外篇《网安融资新星及融资过亿企业介绍》&#xff0c;作为以甲方身份创业的零信任办公安全明星企业&#xff0c;持安科技以网安融资新星…

SQL数据库查询超时,查询数据库的哪些表被上锁的语句

1.异常提示 2.表语句 2.1 查询锁表的语句 select request_session_id spid,OBJECT_NAME(resource_associated_entity_id) tableName from sys.dm_tran_locks where resource_typeOBJECT * 若是下面没有显示内容&#xff0c;说明当前没有锁住的表 2.2若是有显示锁住的表&#…

STM32移植FAT文件系统

所谓“移植”&#xff0c;就是打通FAT源码和物理设备之间的软件接口。 FAT源码早就被公益组织给写好了&#xff0c;直接下载源码。但是FAT作为顶层应用程序&#xff0c;它需要面对的底层物理设备是不确定的&#xff0c;那么底层的物理设备驱动程序就需要程序员来自己写。物理设…

18 矩阵置0

矩阵置0 题解1 首行首列做标志记录&#xff08;原地改数组&#xff09;题解2 位计算 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 提示&#xff1a; m matrix.lengthn matrix[0].length1 …