LRU缓存淘汰算法详解与实现

目录

1.什么是LRU算法

2.LRU算法原题描述

3.LRU算法设计

4.LRU算法细节分析

5.代码实现


1.什么是LRU算法

就是一种缓存淘汰策略

计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢?

LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。

2.LRU算法原题描述

举一个例子:

假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢?

按照 LRU 的策略,就关最底下的,因为那是最久未使用的,然后把新开的应用放到最上面:

现在你应该理解 LRU(Least Recently Used)策略了。

我们先从一道LRU设计算法题开始

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。支持以下操作:

获取数据 get 和 写入数据 put 。获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。

写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

O(1) 时间复杂度内完成这两种操作

3.LRU算法设计

分析上面的操作过程,要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分

因为必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。

那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表

LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:

在双向链表中特意增加两个节点,不用来存储任何数据。使用节点,增加/删除节点的时候就可以不用考虑边界节点不存在情况,简化编程难度,降低代码复杂度。 

思想听起来很简单,就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。回想刚才的例子,这种数据结构是不是完美解决了 LRU 缓存的需求?

也许大家会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 key,为什么链表中还要存键值对呢,只存值不就行了?

这样设计的原因,必须等我们亲自实现 LRU 算法之后才能理解,所以我们开始一步步实现代码吧

4.LRU算法细节分析

新插入的元素或者最新查询的元素要放到链表的头部,对于长时间未访问的元素要放到链表尾部,所以每次插入或者查询都需要维护链表中元素的顺序

使用哈希表的原因是查询时间复杂度为O(1),使用双向链表的原因是对于删除和插入操作时间复杂度为O(1)。

其中哈希表中存储的 key 为 K,value 为 Node<K,V> 的引用,双向链表存储的元素为Node<K,V>的引用.

对于put操作:

①首先判断缓存中 元素 K 是否存在,如果存在,则把链表中的元素Node<K, V>删除,map中的数据<K, Node<K, V> >不用删除,再在链表头部插入元素,并更新map,直接返回即可 ;

②缓存不存在,并且缓存没有满的话,直接把元素插入链表的表头,缓存满了的话移除表尾元素(最旧未访问元素),将元素K插入表头,增加map中的<K, Node<K, V>>, 更新map。

对于get操作:

首先要判断 缓存中(map)是否存在,如果存在则把该节点删除并在链表头部插入该元素并更新map 返回当前元素即可,如果map不存在 则直接返回-1;

5.代码实现

public class LRUCache {Entry head, tail;int capacity;int size;Map<Integer, Entry> cache;public LRUCache(int capacity) {this.capacity = capacity;// 初始化链表initLinkedList();size = 0;cache = new HashMap<>(capacity   2);}/*** 如果节点不存在,返回 -1.如果存在,将节点移动到头结点,并返回节点的数据。** @param key* @return*/public int get(int key) {Entry node = cache.get(key);if (node == null) {return -1;}// 存在移动节点moveToHead(node);return node.value;}/*** 将节点加入到头结点,如果容量已满,将会删除尾结点** @param key* @param value*/public void put(int key, int value) {Entry node = cache.get(key);if (node != null) {node.value = value;moveToHead(node);return;}// 不存在。先加进去,再移除尾结点// 此时容量已满 删除尾结点if (size == capacity) {Entry lastNode = tail.pre;deleteNode(lastNode);cache.remove(lastNode.key);size--;}// 加入头结点Entry newNode = new Entry();newNode.key = key;newNode.value = value;addNode(newNode);cache.put(key, newNode);size  ;}private void moveToHead(Entry node) {// 首先删除原来节点的关系deleteNode(node);addNode(node);}private void addNode(Entry node) {head.next.pre = node;node.next = head.next;node.pre = head;head.next = node;}private void deleteNode(Entry node) {node.pre.next = node.next;node.next.pre = node.pre;}public static class Entry {public Entry pre;public Entry next;public int key;public int value;public Entry(int key, int value) {this.key = key;this.value = value;}public Entry() {}}private void initLinkedList() {head = new Entry();tail = new Entry();head.next = tail;tail.pre = head;}public static void main(String[] args) {LRUCache cache = new LRUCache(2);cache.put(1, 1);cache.put(2, 2);System.out.println(cache.get(1));cache.put(3, 3);System.out.println(cache.get(2));}
}

如果大家完全理解了LRU算法,那么练习结果这个相信大家一眼就可以看出:

分析:

1.首先我们给出的容量是2,先push 1 后push 2,那么现在数据存储顺序应该是2,1

2.我们接下来对于1这个值进行了查询,因为1这个值存在,所以1这个值被引用了一次,1这个值放到最上边,返回1,现在的顺序是1,2

3.现在push数据3,因为容量只有2,所以淘汰掉末尾数据2,现在顺序变为3,1

4.最后查询数据2,因为没有数据2,所以返回-1;顺序不变

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

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

相关文章

tinkerCAD案例:31. 3D 基元形状简介

tinkerCAD案例&#xff1a;31. 3D 基元形状简介 1 将一个想法从头脑带到现实世界是一次令人兴奋的冒险。在 Tinkercad 中&#xff0c;这将从一个新的设计开始。 在新设计中&#xff0c;简单的原始形状可以通过不同的方式组合成更复杂的形状。 在这个项目中&#xff0c;你将探索…

【个人笔记】Linux 服务管理两种方式service和systemctl

service命令与systemctl 命令 service 命令与传统的 SysVinit 和 Upstart 初始化系统相关。较早期的 Linux 发行版&#xff08;如早期的 Ubuntu、Red Hat 等&#xff09;使用了这些初始化系统。service 命令用于启动、停止、重启和查询系统服务的状态。虽然许多现代 Linux 发行…

小红书2020校招测试开发后端笔试题卷三

//完全背包求组合数 #include <iostream> #include<vector> #include<set> #include<map> #include<algorithm> using namespace std; int value[300]; // vector<int>vis; // vector<int>vis1; map<vector<int>,int>m…

web前端开发工程师工作的岗位职责(合集)

web前端开发工程师工作的岗位职责1 职责&#xff1a; 1、根据设计图进行前端页面开发并设计编写业务交互脚本 2、优化前端页面&#xff0c;保证良好的用户体验以及不同浏览器的兼容性 3、web前沿技术研究和新技术调研&#xff0c;将主流的特效应用到业务场景中 4、配合后台…

安装Apache遇到的问题

安装Apache服务 httpd -k install -n Apache2.4 #-n后面表示自定义访问名称 问题1&#xff1a; 此时去 windows 的开始摁扭里找到控制器右键管理员运行 问题2&#xff1a; 命令行没用对 应该用&#xff1a; .\httpd -k install -n Apache2.4 #-n后面表示自定义访问名称

kafka常用命令

目录 Kafka通用命令 进入Kafka 1.进入kafka容器 2.进入kafka目录 查看Topic信息 1.查看所有Topic的列表 2.查看单个Topic的信息 查看ConsumerGroup信息 1.查看所有ConsumerGroup的列表 2.查看单个ConsumerGroup的信息 读取Topic中的数据 向Topic写入数据 Kafka通用…

快速响应,上门维修小程序让您享受无忧生活

随着科技的不断发展和智能手机的普及&#xff0c;上门维修小程序成为了现代人生活中越来越重要的一部分。上门维修小程序通过将维修服务与互联网相结合&#xff0c;为用户提供了更加便捷、高效的维修服务体验。下面将介绍上门维修小程序开发的优势。   提供便捷的预约方式&am…

神经网络原理概述

文章目录 1.神经元和感知器1.1.什么是感知器1.2.什么是单层感知器1.3.多层感知机&#xff08;Multi-Layer Perceptron&#xff0c;MLP&#xff09; 2.激活函数2.1.单位阶跃函数2.2.sigmoid函数2.3.ReLU函数2.4.输出层激活函数 3.损失函数4.梯度下降和学习率5.过拟合和Dropout6.…

Vue3使用vxetable进行表格的编辑、删除与新增

效果图如下: vxetable4传送门 一、引入插件 package.json中加入"vxe-table": "4.0.23",终端中执行npm i导入import {VXETable, VxeTableInstance

docker容器的基本操作

一、查看Docker的版本信息 [roothuyang1 ~]# docker version 二、查看docker的详细信息 [roothuyang1 ~]# docker info 三、Docker镜像操作 Docker创建容器前需要本地存在对应的镜像&#xff0c;如果本地加载不到相关镜像&#xff0c;Docker默认就会尝试从镜像仓库https://hu…

数据可视化与机器学习建模:心力衰竭预测_企业科研_论文科研_毕业设计

数据分析与可视化 心力衰竭或心血管疾病 (CVD) 是全球第一大死因&#xff0c;每年夺去大约1790 万人的生命&#xff0c;占全球所有死亡人数的 31%。 大多数心血管疾病可以通过使用全民策略解决烟草使用、不健康饮食和肥胖、缺乏身体活动和有害使用酒精等行为风险因素来预防…

elementUI 实现动态表单数据校验

转载http://t.csdn.cn/XuTa2 1、探讨需求 首先我们需要探讨一下需求&#xff1a; 表单中的部分el-form-item 的label都是从接口拿到的&#xff0c;需要遍历进行动态渲染。 需要给每个el-form-item加上校验至少是必填校验 有的el-form-item不需要校验&#xff0c;也不是从接口…

@monaco-editor/react组件CDN加载失败解决办法

monaco-editor/react引入这个cdn资源会load失败 网上很多例子都是这样写的&#xff0c;我这样写monaco会报错 import * as monaco from monaco-editor; import { loader } from monaco-editor/react;loader.config({ monaco });改成这样 import * as monaco from monaco-edi…

​​​amoeba实现MySQL读写分离

​​​amoeba实现MySQL读写分离 准备环境&#xff1a;主机A和主机B作主从配置&#xff0c;IP地址为192.168.131.129和192.168.131.130&#xff0c;主机C作为中间件&#xff0c;也就是作为代理服务器&#xff0c;IP地址为192.168.131.136。三台服务器操作系统为RHEL6.4 x86_64,为…

搞活系列-Java NIO之偏偏不用buffer.flip()会出现什么问题?

最近看博客又看到了Java NIO相关的博客&#xff0c;其中有讲解NIO和传统IO关于文件复制的文章&#xff0c;看到了如下的代码&#xff1a; /**** channel用例* 基于channel的文件复制*/Testpublic void fileCopyByChannel(){try {FileInputStream fileInputStream new FileInpu…

TypeScript 【type】关键字的进阶使用方式

导语&#xff1a; 在前面章节中&#xff0c;我们了解到 TS 中 type 这个关键字&#xff0c;常常被用作于&#xff0c;定义 类型别名&#xff0c;用来简化或复用复杂联合类型的时候使用。同时也了解到 为对象定义约束接口类型 的时候所使用的是 Interfaces。 其实对于前面&#…

iOS 应用上架流程详解

iOS 应用上架流程详解 欢迎来到我的博客&#xff0c;今天我将为大家分享 iOS 应用上架的详细流程。在这个数字化时代&#xff0c;移动应用已经成为了人们生活中不可或缺的一部分&#xff0c;而 iOS 平台的 App Store 则是开发者们发布应用的主要渠道之一。因此&#xff0c;了解…

智安网络|常见的网络安全陷阱:你是否掉入了其中?

在数字化时代&#xff0c;网络安全成为了一个重要的议题。随着我们越来越多地在互联网上进行各种活动&#xff0c;诸如在线银行交易、社交媒体分享和在线购物等&#xff0c;我们的个人信息也更容易受到攻击和滥用。虽然有许多关于网络安全的指导和建议&#xff0c;但仍然有许多…

【ChatGPT】ChatGPT是如何训练得到的?

前言 ChatGPT是一种基于语言模型的聊天机器人&#xff0c;它使用了GPT&#xff08;Generative Pre-trained Transformer&#xff09;的深度学习架构来生成与用户的对话。GPT是一种使用Transformer编码器和解码器的预训练模型&#xff0c;它已被广泛用于生成自然语言文本的各种…

【前端知识】React 基础巩固(四十)——Navigate导航

React 基础巩固(四十)——Navigate导航 一、Navigate的基本使用 新建Login页面&#xff0c;在Login中引入Navigate&#xff0c;实现点击登陆按钮跳转至/home路径下&#xff1a; import React, { PureComponent } from "react"; import { Navigate } from "reac…