算法通过村第五关-队列和Hash黄金笔记|LRU的设计与实现

文章目录

  • 前言
  • 1. LRU的含义
  • 2. Hash+双向链表实现LRU
  • 总结


前言


提示:我曾如此渴望命运的波澜,到最后才发现:人生最曼妙的风景,竟是内心的淡定从容。 我们层如此盼望世界的认可,到最后才知道:世界是自己,与他人毫无关系。 --杨绛

LRU 是非常经典的问题,而且在常年的算法中也是热门,但是他是存在技巧的,我们这就来一起看看吧。

1. LRU的含义

缓存是应用软件的必备功能之一,在操作系统中,Java里面的Spring、mybatis,redis、mysql等软件中都有自己的内部缓存模块,然而缓存是如何实现的呢?在操作系统的教科书中,我们知道常见的有FIFO、LRU,LFU三种基本方法。FIFO也就是队列方式,但是它不能很好的利用程序局部性特征,缓存效果比较差,一般我们比较推荐使用LRU(最近最少使用)和LFU(最不经常使用的淘汰算法)。LRU是淘汰最长时间没有被使用的页面,LFU是淘汰一段时间内,使用次数最少的页面。

从实现上来说LRU相对更容易一些,而LFU比较麻烦,我们这里重点研究一下LRU的问题,当然这也是一道高频题目,让我们设计一个LRU缓存,该题可以排到算法前3是没有问题的。

参考题目介绍:146. LRU 缓存 - 力扣(LeetCode)

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
百度百科:

LRU(最近最少使用)缓存:LRU_百度百科 (baidu.com)

首先关于什么是LRU,最简单的说法是当内存空间满了,不得不淘汰某些数据时(通常时容量已满),先择最久未呗使用的数据进行淘汰。

这里时做了简化的,题目是让我们实现一个容器固定LRUCache。如果插入数据时,发现容器已满时,则先按照LRU规则进行淘汰数据,再插入新的数据,这种【插入】和【查询】都算一次“使用”。

最近最少使用算法(LRU)是大部分操作系统为最大化页面命中率而广泛使用的一种页面置换算法。

该算法的思路是:

发生缺页中断时,选择未使用时间最长的页面置换出去。假设内存只能容纳3个页大小,按照7 0 1 2 0 3 0 4 的次序访问页面。假设内存按照栈的方式类描述访问时间,在上面时最近访问的,在下面的最远时间访问的,LRU就是这样工作的:
在这里插入图片描述
尝试了工作流程,那么我们需要怎么来实现它呢,有什么好的思路呢?定义一个数组,然后根据上面的写上一些规则吗?我估计这个要实现起来会非常难,即使写出来了,也非常复杂,超时等。那我们应该怎么做,这里告诉你标准的答案:Hash+双向链表。

2. Hash+双向链表实现LRU

目前公认最合理的方法,Hash+双向链表。没想到吧?那我们想一下为什么这么设计,我们要怎么实现。(展示💕

  • Hash的作用是:采用O(1)访问元素,哈希表就是普通的哈希映射(HashMap),通过缓存数据的键映射到其双向链表的位置。Hash里的数据结构是key-value,value就是我们自己封装好的node,key就是键值,也就是在Hash的地址
  • 双向链表的作用是:根据访问情况对元素进行排序。双向链表按照被使用的顺序存储这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。

这样以来,我们要确定元素的位置就可以直接访问哈希表就可以,找出缓存项的双向链表中的位置,随后将其移动到双向链表的头部,即可以实现O(1)的时间内完成get操作或者put操作。具体的方法放在下面💡:

  • 对于get操作,首先判断key是否存在:
    • 如果key不存在,则返回-1
    • 如果key存在,则key对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值
  • 对于put操作,首先要判断key是否存在:
    • 如果key不存在,使用key和value创建一个新的节点,在双向链表的头部添加该节点,并将key和该节点添加进哈希表中。然后判断双向链表的节点书是否超出容量,如果超出容量,则删除双向链表的尾节点,并删除哈希表中的对应项
    • 如果key存在,则于get操作类似,先通过哈希定位,再将对应的节点更新为value,并将该节点移到双向链表的头部。

上述的各项操作中,访问哈希表的时间复杂度为O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也是O(1)。而将一个节点移到双向链表的头部,可以分成【删除该节点】和【在双向链表的头部添加元素】两部操作,都可以在O(1)的时间内完成。

同时为了方便操作,在双向链表的实现中,使用伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。

说了这么多,还是看图更方便一些:

双向链表的实现中,使用伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。

我们先看这个容量为3 的例子,首先缓存了1,此时结构如图所示,之后缓存2,3结构如b所示:

在这里插入图片描述
之后4再进入,此时容量已经不够了,只能再最远未使用的元素1删掉,然后将4插入到链表头部,如此就变成了图c的样子。

如果接下类又访问了一次2,会怎么样呢?我们会将2移动到链表的首位,也就是下面d的样子。
在这里插入图片描述
之后存储5呢?此时将tail指向的3删除,然后将5插入到链表的头部,也就是如上图的e的样子。

上面的方案图示很容易实现,但是这里我们强调几个点哈🥰:

  1. 假设容量没有满,可以直接将元素插入到链表头部就可以
  2. 如果容量满了,就要采取策略,新的元素到来,则tail指向的表尾元素删除就行
  3. 假设要访问的元素已经存在,则将此元素先从链表中删除,再插入到表头就行了。

我们再看一些Hash的操作:

  1. Hash没有容量的限制,凡是被访问的元素都会再Hash中有标记,key就是我们要查询的条件,而value就是链表的节点的引用,可以不访问链表直接定位到某个元素节点,然后执行我们上面提到的方法来删除对应的元素节点。
  2. 这里的双向链表的删除好理解,那HashMap中的删除要如何处理呢?其实就是将node变为null。这样get(key)的时候就会返回null,也就实现了删除的功能。

总结:

上述的各项操作中,访问哈希表的时间复杂度为O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也是O(1)。而将一个节点移到双向链表的头部,可以分成【删除该节点】和【在双向链表的头部添加元素】两部操作,都可以在O(1)的时间内完成。

代码展示看效果
难度:⭐⭐⭐⭐⭐

import java.util.HashMap;
import java.util.Map;public class LRUCache {// 写一个双向链表的结构class DLinkedNode {int key;int value;// 前后引用DLinkedNode prev;DLinkedNode next;public DLinkedNode() {}public DLinkedNode(int _key, int _value) {this.key = _key;this.value = _value;}}// 需要一个HashMapprivate Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();// 需要一个sizeprivate int size;// 需要一个容量private int capacity;// 需要头尾伪节点private DLinkedNode head, tail;public LRUCache(int capacity) {this.size = 0;this.capacity = capacity;// 使用伪 头节点和尾节点head = new DLinkedNode();tail = new DLinkedNode();// 相互联系head.next = tail;tail.prev = head;}public int get(int key) {DLinkedNode node = cache.get(key);if (node == null) {return -1;}// 这里如果key存在,先通过哈希表定位在移动到头部moveToHead(node);return node.value;}public void put(int key, int value) {DLinkedNode node = cache.get(key);if (node == null) {// key 不存在就创建一个新的节点DLinkedNode newNode = new DLinkedNode(key, value);// 添加到哈希表中cache.put(key, newNode);// 添加到双向链表的头部addToHead(newNode);++size;if (size > capacity) {// 超出容量,对应策略 删除双向链表的尾部节点DLinkedNode tail = removeTail();// 删除对应哈希表中的项cache.remove(tail.key);--size;}} else {// 如果key存在  先通过hash定位,再修改value,并且将它移动到头部node.value = value;moveToHead(node);}}private void addToHead(DLinkedNode node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;}private void removeNode(DLinkedNode node) {node.prev.next = node.next;node.next.prev = node.prev;}private void moveToHead(DLinkedNode node) {removeNode(node);addToHead(node);}private DLinkedNode removeTail() {DLinkedNode res = tail.prev;removeNode(res);return res;}public static void main(String[] args) {LRUCache lRUCache = new LRUCache(2);lRUCache.put(1, 1); // 缓存是 {1=1}lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}System.out.println(lRUCache.get(1));    // 返回 1lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}System.out.println(lRUCache.get(2));    // 返回 -1 (未找到)lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}System.out.println(lRUCache.get(1));    // 返回 -1 (未找到)System.out.println(lRUCache.get(3));    // 返回 3System.out.println(lRUCache.get(4));    // 返回 4}
}

总结

提示:理解双向链表;了解LRU缓存机制;Hash+双向链表的思考

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

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

相关文章

罕见病 对称性脂肪瘤(MSL) 马德龙病

如果你体内脂肪瘤分布大致如下 而且个数不断增多 这篇文章适合你 症状 脂肪瘤个数一直增加 而且很对称 比如: 左手臂一个 右手臂一个 别名 多发性对称性脂肪增多症 Multiple symmetric lipomatosis (MSL) 多发性对称性脂肪瘤&#xff08;MSL&#xff09; 脂肪瘤 马德龙病(…

知识库建设:从0到1搞定知识库建设的方法论分享

如果我们想要搭建一个知识库&#xff0c;前提是我们要明确知道这个知识库是干什么用的&#xff0c;只有了解知识库的应用场景才能知道如何去建设知识库。 知识库建设 以常见的电商客服为例&#xff0c;客户会经常咨询什么时候发货&#xff0c;怎么退货&#xff0c;怎么换货………

编写软件检测报告有哪些注意事项?软件检测报告获取

软件检测报告是指把测试的过程和结果写成文档&#xff0c;对发现的问题和缺陷进行分析&#xff0c;为纠正软件的存在的质量问题提供依据&#xff0c;同时为软件验收和交付打下基础。 一、编写软件检测报告的注意事项 1、报告的结构要合理和清晰。应该按照一定的逻辑顺序&…

RabbitMQ工作模式-主题模式

主题模式 官方文档参考&#xff1a;https://www.rabbitmq.com/tutorials/tutorial-five-python.html 使用topic类型的交换器&#xff0c;队列绑定到交换器、bingingKey时使用通配符&#xff0c;交换器将消息路由转发到具体队列时&#xff0c;会根据消息routingKey模糊匹配&am…

ardupilot 安装gcc-arm-none-eabi编译工具

目录 文章目录 目录摘要0简介1.下载网站2.安装摘要 本节主要记录ardupilot使用的编译器安装过程。 0简介 gcc-arm-none-eabi是GNU项目下的软件,是一个面向裸机arm的编译器。那么说了这么多介绍,它都包含什么具体功能又怎么安装与使用呢,我们继续。 1.下载网站 gcc-arm-n…

存储过程报Illegal mix of collations错误的解决方法

CREATE PROCEDURE maxAgeStudent(IN _gender CHAR) BEGINDECLARE maxage INT DEFAULT 0;SELECT max(age) INTO maxage FROM student where gender _gender;SELECT * from student WHERE age maxage and gender _gender; END; 在调用的时候 call maxAgeStudent(1) 产生了报…

git快速查看某个文件修改的所有commit

1. git blame file git blame 可以显示历史修改的每一行记录,有时候我们只想了解某个文件一共提交几次commit,只显示commit列表,这种方式显然不满足要求。 2.git log常规使用 (1)显示整个project的所有commit (2)显示某个文件的所有commit 这是git log不添加参数的常规…

c语言每日一练(14)【加强版】

前言&#xff1a;每日一练系列&#xff0c;每一期都包含5道选择题&#xff0c;2道编程题&#xff0c;博主会尽可能详细地进行讲解&#xff0c;令初学者也能听的清晰。博主有时会将一些难题综合成每日一练加强版&#xff0c;加强版是特殊的&#xff0c;它仅包含5道选择题&#x…

ChatGPT集锦

目录 1. 一条指令让ChatGPT变的更强大2. 对ChatGPT提问时,常见的10种错误描述3. Custom instructions如何设置1. 一条指令让ChatGPT变的更强大 在使用GPT的过程中,如何让AI更清晰地了解你的需求很重要?今天分享一个指令,可以让GPT成为你的好同事,与你一起分析和解决问题,…

线性空间、子空间、基、基坐标、过渡矩阵

线性空间的定义 满足加法和数乘封闭。也就是该空间的所有向量都满足乘一个常数后或者和其它向量相加后仍然在这个空间里。进一步可以理解为该空间中的所有向量满足加法和数乘的组合封闭。即若 V 是一个线性空间&#xff0c;则首先需满足&#xff1a; 注&#xff1a;线性空间里面…

智能小车—PWM方式实现小车调速和转向

目录 1. 让小车动起来 2. 串口控制小车方向 3. 如何进行小车PWM调速 4. PWM方式实现小车转向 1. 让小车动起来 电机模块开发 L9110s概述 接通VCC&#xff0c;GND 模块电源指示灯亮&#xff0c; 以下资料来源官方&#xff0c;具体根据实际调试 IA1输入高电平&#xff0c…

AR产业变革中的“关键先生”和“关键力量”

今年6月的WWDC大会上&#xff0c;苹果发布了头显产品Vision Pro&#xff0c;苹果CEO库克形容它&#xff1a; 开启了空间计算时代。 AR产业曾红极一时&#xff0c;但因为一些技术硬伤又减弱了声量&#xff0c;整个产业在起伏中前行。必须承认&#xff0c;这次苹果发布Vision P…