LeetCode题练习与总结:LRU缓存--146

一、题目描述

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

提示:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 10^5
  • 最多调用 2 * 10^5 次 get 和 put

二、解题思路

为了实现LRU缓存,我们需要一个能够快速访问、更新和删除元素的数据结构。哈希表提供快速的查找时间,但是它不能保持元素的顺序。而双向链表可以保持元素的顺序,但是查找、更新和删除操作不是快速的。因此,我们可以结合使用哈希表和双向链表来实现LRU缓存。

以下是解题思路:

  1. 使用哈希表来存储键和对应的节点(而不是值),这样我们可以在O(1)时间内找到节点。
  2. 使用双向链表来维护键的顺序,当某个键被访问时,将其移动到链表的头部;当插入新键时,如果缓存已满,则删除链表尾部的节点,并在头部插入新节点。
  3. 自定义一个双向链表的节点类Node,包含keyvalue以及前驱和后继节点的指针。
  4. LRUCache类中,维持对头部和尾部节点的引用,以便在O(1)时间内进行删除和添加操作。

三、具体代码

import java.util.HashMap;class Node {int key, value;Node prev, next;public Node(int key, int value) {this.key = key;this.value = value;}
}public class LRUCache {private HashMap<Integer, Node> map;private Node head, tail;private int capacity, count;public LRUCache(int capacity) {this.capacity = capacity;this.count = 0;map = new HashMap<>();head = new Node(0, 0);tail = new Node(0, 0);head.next = tail;tail.prev = head;}public int get(int key) {Node node = map.get(key);if (node == null) {return -1;}moveToHead(node);return node.value;}public void put(int key, int value) {Node node = map.get(key);if (node == null) {Node newNode = new Node(key, value);map.put(key, newNode);addNode(newNode);count++;if (count > capacity) {Node toDel = popTail();map.remove(toDel.key);count--;}} else {node.value = value;moveToHead(node);}}private void addNode(Node node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;}private void removeNode(Node node) {node.prev.next = node.next;node.next.prev = node.prev;}private void moveToHead(Node node) {removeNode(node);addNode(node);}private Node popTail() {Node res = tail.prev;removeNode(res);return res;}
}

四、时间复杂度和空间复杂度

1. 时间复杂度
  • get(int key) 方法的时间复杂度是 O(1)。这是因为我们首先在哈希表中查找键,这需要 O(1) 时间。如果找到了节点,我们将其移动到链表的头部,这个操作也是 O(1) 时间,因为它只涉及到几个节点的指针变化。

  • put(int key, int value) 方法的时间复杂度同样是 O(1)。在最好的情况下,我们只需要在哈希表中插入一个新节点并把它添加到链表头部,这需要 O(1) 时间。在最坏的情况下,我们需要先删除链表尾部的节点,然后再将新节点添加到链表头部,这些操作也都是 O(1) 时间。

  • addNode(Node node)removeNode(Node node) 和 moveToHead(Node node) 方法的时间复杂度都是 O(1),因为它们只涉及到链表中的常数个节点的指针操作。

  • popTail() 方法的时间复杂度也是 O(1),因为它只是返回链表尾部的节点并将其从链表中移除,这些操作都是 O(1) 时间。

2. 空间复杂度
  • LRUCache 类的空间复杂度主要由哈希表和双向链表组成。哈希表的空间复杂度是 O(capacity),因为哈希表中最多只能存储 capacity 个键值对。

  • 双向链表的空间复杂度也是 O(capacity),因为链表中最多只能有 capacity 个节点。

  • 因此,LRUCache 类的总空间复杂度是 O(capacity),即与缓存容量成线性关系。

综上所述,LRUCache 类的 get 和 put 方法的时间复杂度都是 O(1),空间复杂度是 O(capacity)。

五、总结知识点

1. 数据结构:

  • HashMap: 用于存储键值对,提供快速的查找、插入和删除操作。
  • 双向链表: 由Node类实现,用于维护元素的访问顺序,支持快速的节点插入和删除。

2. 链表操作:

  • 链表节点的添加(addNode)和删除(removeNode)操作。
  • 将节点移动到链表头部(moveToHead),以标记为最近使用。
  • 删除链表尾部的节点(popTail),以实现LRU淘汰策略。

3. 缓存算法:

  • LRU (Least Recently Used) 缓存算法: 当缓存达到容量上限时,优先淘汰最久未使用的元素。

4. 设计模式:

  • 迭代器模式: 双向链表可以看作是迭代器模式的一个实例,它允许遍历元素而无需暴露其内部表示。

5. 编程技巧:

  • 使用伪头部和伪尾部节点来简化链表操作,避免空指针异常和边界条件的处理。
  • put方法中,先检查节点是否存在,然后决定是更新值还是添加新节点。

6. 封装:

  • LRUCache类封装了所有的实现细节,对外只暴露了getput两个方法,遵循了良好的封装原则。

7. 错误处理:

  • get方法中,如果键不存在于缓存中,返回-1作为错误指示。

以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。

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

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

相关文章

Linux中cat命令的英文含义

我之前一直在想cat不是猫的意思吗&#xff0c;但是cat命令在Linux中并不是指"猫"这个动物&#xff0c;而是来源于它的功能&#xff1a;concatenate&#xff08;连接&#xff09;和typeset&#xff08;打印&#xff09;。这个命令的名称是这两个功能的首字母缩写。尽管…

DevExpress WPF中文教程:Grid - 如何显示摘要(设计时)?

DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序&#xff0c;这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件…

Mac/Linux安装JMeter压测工具

Mac安装JMeter压测工具 介绍 Apache JMeter™应用程序是开源软件&#xff0c;是一个100%纯的Java应用程序&#xff0c;旨在加载测试功能行为和衡量性能。它最初是为测试Web应用程序而设计的&#xff0c;但后来扩展到其他测试功能。 我能用它做什么&#xff1f; Apache JMet…

如何设计一个秒杀系统,(高并发高可用分布式集群)

设计一个高并发、高可用的分布式秒杀系统是一个非常具有挑战性的任务&#xff0c;需要从架构、数据库、缓存、并发控制、降级限流等多个维度进行考虑。以下是一个典型的秒杀系统设计思路&#xff1a; 1. 系统架构 微服务架构 拆分服务&#xff1a;将系统功能拆分为多个微服务…

vue高德地图使用

先根据官方方法给vue项目引入高德 高德文档地址 做好准备后使用 初始化地图 AMap.plugin(AMap.MoveAnimation, () >{//地图this.map new AMap.Map("mapContainer", {resizeEnable: true,center: [116.397447,39.909176],//地图中心坐标zoom:12,//缩放值});this.…

Appium+python自动化(三十九)-Appium自动化测试框架综合实践 - 代码实现(超详解)

1.简介 今天我们紧接着上一篇继续分享Appium自动化测试框架综合实践 - 代码实现。由于时间的关系&#xff0c;宏哥这里用代码给小伙伴演示两个模块&#xff1a;注册和登录。 2.业务模块封装 因为现在各种APP的层出不群&#xff0c;各式各样的。但是其大多数都有注册、登录。为…

走在健康前沿:低GI食品认证与现代饮食的新篇章

随着现代社会节奏的加快&#xff0c;人们对健康饮食的追求也日益增强。在众多饮食理念中&#xff0c;低血糖生成指数&#xff08;GI&#xff09;食品凭借其对控制血糖和预防慢性疾病的潜在益处&#xff0c;逐渐成为健康饮食领域的明星。 GI的科学解码 GI&#xff0c;即食物血糖…

CTFHUB-SSRF-URL Bypass

开启题目 给出提示&#xff0c;url参数的值中必须包含有 http://notfound.ctfhub.com &#xff0c;可以采用&#xff0c;也就是 HTTP 基本身份认证绕过 HTTP 基本身份认证允许 Web 浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式。 也就是…

AIGC文生图lora微调训练案例;SD-Train界面训练stable Diffusion lora模型

lora仓库&#xff08;1000多个lora微调模型分享&#xff09;&#xff1a; https://lorastudio.co/models 1、命令代码方式&#xff1a;文生图lora微调训练案例 主要用huggingface相关包&#xff1a;peft、accelerate、diffusers 参考&#xff1a; https://huggingface.co/blo…

Swift 6:导入语句上的访问级别

文章目录 前言示例启用 AccessLevelOnImport破坏性变更采用这些更改总结前言 SE-0409 提案引入了一项新功能,即允许使用 Swift 的任何可用访问级别标记导入声明,以限制导入的符号可以在哪些类型或接口中使用。由于这些变化,现在可以将依赖项标记为对当前源文件(private 或…

【什么是 可重入锁】

不可重入锁示例&#xff1a; public class Lock{private boolean isLocked false;public synchronized void lock() throws InterruptedException{while(isLocked){ wait();}isLocked true;}public synchronized void unlock(){isLocked false;notify();}}public class …

k8s离线安装单节点elasticsearch7.x

目录 概述资源实践脚本 概述 k8s离线安装单节点elasticsearch7.x 资源 镜像可以自己准备&#xff0c;懒人速递 elasticsearch离线安装镜像-版本7.17.22 实践 脚本 # pvc apiVersion: v1 kind: PersistentVolumeClaim metadata:name: es-nfsnamespace: defaultlabels:pvc: …

数据结构排序算法(图示突然传不上来,后面再更新)

排序算法的选择 需要考虑的因素有以下四点: 待排序的记录数目n的大小; 记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小; 关键字的结构及其分布情况; 对排序稳定性的要求。通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个…

PostgreSQL的pg_filedump工具

PostgreSQL的pg_filedump工具 基础信息 OS版本&#xff1a;Red Hat Enterprise Linux Server release 7.9 (Maipo) DB版本&#xff1a;16.2 pg软件目录&#xff1a;/home/pg16/soft pg数据目录&#xff1a;/home/pg16/data 端口&#xff1a;5777pg_filedump 是一个工具&#x…

JAVA小知识30:JAVA多线程篇1,认识多线程与线程安全问题以及解决方案。(万字解析)

来 多线程&#xff0c;一个学起来挺难但是实际应用不难的一个知识点&#xff0c;甚至在很多情况下都不需要考虑&#xff0c;最多就是写测试类的时候模拟一下并发&#xff0c;现在我们就来讲讲基础的多线程知识。 一、线程和进程、并发与并行 1.1、线程和进程 线程&am…

Java学习十二—Java8特性之Optional类

一、简介 Java 8 引入了 Optional​ 类作为一种容器&#xff0c;可以用来显式地表示一个值存在或不存在。它解决了传统上可能会遇到的空指针异常问题&#xff0c;同时提供了一种更优雅的方式来处理可能为null的情况。 Java 8 中引入 Optional​ 类的背景可以从以下几个方面来理…

可持久化线段树/平衡树

可持久化&#xff1a; 指的是我们每对树做一次修改&#xff0c;就将其保存为一个历史版本&#xff0c;在以后的询问/修改中&#xff0c;我可以选择任意一个历史版本来询问/修改 实现&#xff1a; 最简单的实现思路&#xff1a; 最简单的思路当然是每次修改的时候都将历史版本…

线程池概念的详解

前言&#x1f440;~ 上一章我们介绍了什么是定时器以及如何去实现一个定时器&#xff0c;今天我们来讲解在多线程中同样很重要的一个内容线程池 线程池的出现 线程池概念 标准库中的线程池 工厂模式 newCacheThreadPool方法 newFixedThreadPool方法 ThreadPoolExecutor…

Akamai+Noname强强联合 | API安全再加强

最近&#xff0c;Akamai正式完成了对Noname Security的收购。本文我们将向大家介绍&#xff0c;经过本次收购后&#xff0c;Akamai在保护API安全性方面的后续计划和未来愿景。 Noname Security是市场上领先的API安全供应商之一&#xff0c;此次收购将让Akamai能更好地满足日益增…

图像基础知识

图像卷积 卷积(convolution)是通过两个函数f和g生成第三个函数的一种数学算子,表征函数f与g经过翻转和平移的重叠部分的面积。 卷积概念是两个变量在某范围内相乘后求和的结果。图像处理中的卷积概念:数字图像是一个二维的离散信号,对数字图像做卷积操作其实就是利用卷积…