哈希表题目:LRU 缓存

news/2024/11/15 3:31:13/

文章目录

  • 题目
    • 标题和出处
    • 难度
    • 题目描述
      • 要求
      • 示例
      • 数据范围
  • 前言
  • 解法
    • 思路和算法
    • 代码
    • 复杂度分析

题目

标题和出处

标题:LRU 缓存

出处:146. LRU 缓存

难度

7 级

题目描述

要求

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

实现 LRUCache \texttt{LRUCache} LRUCache 类:

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

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

示例

示例 1:

输入:
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] \texttt{["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]} ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] \texttt{[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]} [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, null, -1, 3, 4] \texttt{[null, null, null, 1, null, -1, null, -1, 3, 4]} [null, null, null, 1, null, -1, null, -1, 3, 4]
解释:
LRUCache lruCache = new LRUCache(2); \texttt{LRUCache lruCache = new LRUCache(2);} LRUCache lruCache = new LRUCache(2);
lruCache.put(1, 1); \texttt{lruCache.put(1, 1);} lruCache.put(1, 1); // 缓存是 {1=1} \texttt{\{1=1\}} {1=1}
lruCache.put(2, 2); \texttt{lruCache.put(2, 2);} lruCache.put(2, 2); // 缓存是 {1=1, 2=2} \texttt{\{1=1, 2=2\}} {1=1, 2=2}
lruCache.get(1); \texttt{lruCache.get(1);} lruCache.get(1); // 返回 1 \texttt{1} 1
lruCache.put(3, 3); \texttt{lruCache.put(3, 3);} lruCache.put(3, 3); // LRU 关键字是 2 \texttt{2} 2,逐出关键字 2 \texttt{2} 2,缓存是 {1=1, 3=3} \texttt{\{1=1, 3=3\}} {1=1, 3=3}
lruCache.get(2); \texttt{lruCache.get(2);} lruCache.get(2); // 返回 -1 \texttt{-1} -1(未找到)
lruCache.put(4, 4); \texttt{lruCache.put(4, 4);} lruCache.put(4, 4); // LRU 关键字是 1 \texttt{1} 1,逐出关键字 1 \texttt{1} 1,缓存是 {4=4, 3=3} \texttt{\{4=4, 3=3\}} {4=4, 3=3}
lruCache.get(1); \texttt{lruCache.get(1);} lruCache.get(1); // 返回 -1 \texttt{-1} -1(未找到)
lruCache.get(3); \texttt{lruCache.get(3);} lruCache.get(3); // 返回 3 \texttt{3} 3
lruCache.get(4); \texttt{lruCache.get(4);} lruCache.get(4); // 返回 4 \texttt{4} 4

数据范围

  • 1 ≤ capacity ≤ 3000 \texttt{1} \le \texttt{capacity} \le \texttt{3000} 1capacity3000
  • 0 ≤ key ≤ 10 4 \texttt{0} \le \texttt{key} \le \texttt{10}^\texttt{4} 0key104
  • 0 ≤ value ≤ 10 5 \texttt{0} \le \texttt{value} \le \texttt{10}^\texttt{5} 0value105
  • 最多调用 2 × 10 5 \texttt{2} \times \texttt{10}^\texttt{5} 2×105 get \texttt{get} get put \texttt{put} put

前言

这道题要求在 O ( 1 ) O(1) O(1) 时间内执行 get \textit{get} get put \textit{put} put 操作,满足该时间复杂度的数据结构是哈希表,即 Java 中的 HashMap \texttt{HashMap} HashMap。但是这道题还要求在关键字数量超出容量时逐出最久未使用的关键字,因此要求关键字按照访问顺序排列。由于 HashMap \texttt{HashMap} HashMap 中的关键字顺序不是按照访问顺序排列,因此需要将 HashMap \texttt{HashMap} HashMap 和双向链表结合才能实现题目的要求。

Java 中有 LinkedHashMap \texttt{LinkedHashMap} LinkedHashMap 类,该类是 HashMap \texttt{HashMap} HashMap 类的子类。和 HashMap \texttt{HashMap} HashMap 类相比, LinkedHashMap \texttt{LinkedHashMap} LinkedHashMap 类维护了双向链表,可以指定迭代顺序是插入顺序或者访问顺序。只要将迭代顺序指定为访问顺序,即可使用 LinkedHashMap \texttt{LinkedHashMap} LinkedHashMap 实现 LRU 缓存。

使用 LinkedHashMap \texttt{LinkedHashMap} LinkedHashMap 实现 LRU 缓存虽然可行,但是不适合在面试中使用。实现 LRU 缓存的标准解法是使用 HashMap \texttt{HashMap} HashMap 和双向链表自行实现,这也是面试官期望看到的解法。

以下是使用 LinkedHashMap \texttt{LinkedHashMap} LinkedHashMap 实现 LRU 缓存的代码,由于不适合在面试中使用,因此不具体说明。

class LRUCache {private class LRUMap extends LinkedHashMap<Integer, Integer> {private int capacity;public LRUMap(int capacity) {super(capacity, 0.75f, true);this.capacity = capacity;}@Overrideprotected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {return size() > capacity; }}private LRUMap map;public LRUCache(int capacity) {map = new LRUMap(capacity);}public int get(int key) {return map.getOrDefault(key, -1);}public void put(int key, int value) {map.put(key, value);}
}

解法

思路和算法

实现 LRU 缓存需要使用哈希表和双向链表。双向链表中的每个结点存储键值对以及该结点的前一个结点和后一个结点,结点顺序为从头结点到尾结点依次按照最近访问时间降序排列。哈希表存储每个关键字对应的双向链表中的结点。

为了便于操作,双向链表需要维护伪头结点和伪尾结点,链表的实际头结点为伪头结点的后一个结点,链表的实际尾结点为伪尾结点的前一个结点(只有当链表不为空时才存在实际头结点和实际尾结点)。初始时,伪头结点和伪尾结点相邻。

构造方法中,将容量初始化为参数值,将哈希表初始化,将双向链表初始化为包含伪头结点和伪尾结点。

对于 get \textit{get} get 操作,做法如下。

  • 如果哈希表中存在关键字 key \textit{key} key,则在哈希表中得到 key \textit{key} key 对应的结点,将该结点移动到双向链表的头部,返回结点的值。

  • 如果哈希表中不存在关键字 key \textit{key} key,返回 − 1 -1 1

由于 get \textit{get} get 操作只是根据关键字 key \textit{key} key 从缓存中得到对应的值,虽然改变了关键字的顺序,但是并没有添加新的关键字,因此关键字的数量不变,不会出现关键字数量超过容量的情况,不需要逐出最久未使用的关键字。

对于 put \textit{put} put 操作,参考 get \textit{get} get 操作,必要的操作如下。

  • 如果哈希表中存在关键字 key \textit{key} key,则在哈希表中得到 key \textit{key} key 对应的结点,将该结点的值设为 value \textit{value} value,并将该结点移动到双向链表的头部。

  • 如果哈希表中不存在关键字 key \textit{key} key,则根据 key \textit{key} key value \textit{value} value 创建结点,将该结点添加到双向链表的头部,并在哈希表中添加 key \textit{key} key 和该结点的对应关系。

上述操作并不完整。和 get \textit{get} get 操作不同, put \textit{put} put 操作可能添加新的关键字,因此需要判断关键字数量是否超过容量,在关键字数量超过容量的情况下需要逐出最久未使用的关键字。

  • 如果哈希表中存在关键字 key \textit{key} key,则 put \textit{put} put 操作只是更新 key \textit{key} key 对应的值和改变关键字的顺序,并没有添加新的关键字,因此关键字的数量不变,不会出现关键字数量超过容量的情况,不需要逐出最久未使用的关键字。

  • 如果哈希表中不存在关键字 key \textit{key} key,则 put \textit{put} put 操作添加了新的关键字,因此关键字的数量增加了 1 1 1 个,此时如果哈希表中的关键字数量超过容量,则需要逐出最久未使用的关键字,具体做法是:得到伪尾结点的前一个结点 tail \textit{tail} tail,该结点是实际尾结点,将实际尾结点从双向链表中删除,并将实际尾结点的关键字从哈希表中删除。

get \textit{get} get put \textit{put} put 操作中,对双向列表的具体操作包括:将结点移动到双向链表的头部、将结点添加到双向链表的头部和将结点从双向链表中删除。由于将结点移动到双向链表的头部的操作可以通过将结点从双向链表中删除和将结点添加到双向链表的头部实现,因此对双向列表的具体操作包括将结点从双向链表中删除和将结点添加到双向链表的头部。

将结点从双向链表中删除的操作如下:定位到待删除结点的前一个结点 prev \textit{prev} prev 和后一个结点 next \textit{next} next,将 prev \textit{prev} prev 的后一个结点设为 next \textit{next} next,将 next \textit{next} next 的前一个结点设为 prev \textit{prev} prev。由于待删除结点一定不是伪头结点和伪尾结点,因此 prev \textit{prev} prev next \textit{next} next 一定不为空,不需要判断是否为空。

将结点添加到双向链表的头部的操作如下:定位到伪头结点和伪头结点的后一个结点即原实际头结点(在添加新结点之前的实际头结点),将待添加结点添加到伪头结点和原实际头结点之间,将待添加结点的前一个结点和后一个结点分别设为伪头结点和原实际头结点,将伪头结点的后一个结点设为待添加结点,将原实际头结点的前一个结点设为待添加结点,即完成添加操作。由于伪头结点和原实际头结点一定不为空,因此不需要判断是否为空。当链表为空时,虽然在添加新节点之前不存在实际头结点,但是可以将伪尾结点看成原实际头结点,将结点添加到双向链表的头部的逻辑是一样的。

对于将结点从双向链表中删除和将结点添加到双向链表的头部的操作,定位到相应结点以及更新相应结点的前一个结点和后一个结点的时间都是 O ( 1 ) O(1) O(1),因此将结点从双向链表中删除和将结点添加到双向链表的头部的操作的时间都是 O ( 1 ) O(1) O(1)。又由于哈希表操作的时间都是 O ( 1 ) O(1) O(1),因此 get \textit{get} get 操作和 put \textit{put} put 操作的时间复杂度都是 O ( 1 ) O(1) O(1)

代码

class LRUCache {private class Node {private int key;private int value;private Node prev;private Node next;public Node() {this(-1, -1);}public Node(int key, int value) {this.key = key;this.value = value;prev = null;next = null;}public int getKey() {return key;}public int getValue() {return value;}public void setValue(int value) {this.value = value;}public Node getPrev() {return prev;}public void setPrev(Node prev) {this.prev = prev;}public Node getNext() {return next;}public void setNext(Node next) {this.next = next;}}private int capacity;private Map<Integer, Node> map;private Node pseudoHead;private Node pseudoTail;public LRUCache(int capacity) {this.capacity = capacity;map = new HashMap<Integer, Node>();pseudoHead = new Node();pseudoTail = new Node();pseudoHead.setNext(pseudoTail);pseudoTail.setPrev(pseudoHead);}public int get(int key) {if (map.containsKey(key)) {Node node = map.get(key);remove(node);addAtHead(node);return node.getValue();} else {return -1;}}public void put(int key, int value) {if (map.containsKey(key)) {Node node = map.get(key);node.setValue(value);remove(node);addAtHead(node);} else {Node node = new Node(key, value);addAtHead(node);map.put(key, node);if (map.size() > capacity) {Node tail = pseudoTail.getPrev();map.remove(tail.getKey());remove(tail);}}}private void remove(Node node) {Node prev = node.getPrev(), next = node.getNext();prev.setNext(next);next.setPrev(prev);}private void addAtHead(Node node) {Node prevHead = pseudoHead.getNext();node.setPrev(pseudoHead);node.setNext(prevHead);pseudoHead.setNext(node);prevHead.setPrev(node);}
}

复杂度分析

  • 时间复杂度:构造方法和各项操作的时间复杂度都是 O ( 1 ) O(1) O(1)

  • 空间复杂度: O ( capacity ) O(\textit{capacity}) O(capacity),其中 capacity \textit{capacity} capacity 是 LRU 缓存的容量。每次 get \textit{get} get put \textit{put} put 操作之后,哈希表中的键值对数量和双向链表中的结点数量(不包括伪头结点和伪尾结点)不会超过 capacity \textit{capacity} capacity。在 put \textit{put} put 操作过程中,哈希表中的键值对数量和双向链表中的结点数量(不包括伪头结点和伪尾结点)不会超过 capacity + 1 \textit{capacity} + 1 capacity+1,且如果达到 capacity + 1 \textit{capacity} + 1 capacity+1,则将删除双向链表中的实际尾结点和该结点的关键字在哈希表中的键值对,使哈希表中的键值对数量和双向链表中的结点数量(不包括伪头结点和伪尾结点)变成 capacity \textit{capacity} capacity


http://www.ppmy.cn/news/74223.html

相关文章

【自己更换模型】如何用 Serverless 一键部署 Stable Diffusion?

作者&#xff1a;寒斜 上一篇讲了如何使用 Serverless Devs 和函数计算快速体验部署 Stable Diffusion&#xff0c;本篇继续聊聊如何解决动态模型加载的问题&#xff0c;从玩起来到用起来。 思路 其实很简单&#xff0c; 我们只需要将镜像里面的动态路径映射到 NAS [ 1] 文件…

UnityEngine.EventSystems详解

介绍 UnityEngine.EventSystems是Unity中的一个核心模块&#xff0c;用于处理用户输入事件和交互。它提供了许多接口和类来处理UI元素&#xff0c;例如按钮、滑动条、文本框等。使用该模块可以方便地实现用户界面的交互和响应。 方法 接口 IPointerClickHandler 当用户点击…

亚马逊产品开发

【一、找产品灵感】 需要不停的去找产品灵感&#xff0c;发起需求&#xff0c;我们到底是要做什么产品?当你看到一个产品&#xff0c;看到一个东西&#xff0c;应该先来考虑这个东西它的尺寸重量&#xff0c;以及在物流方面大概的成本会是多少&#xff0c;你能不能承受?然后…

自动构建之CMake

CMake 链接: 自动构建之MakeFile CMake也是一种用于自动化构建软件项目的工具。Cmake可以自动输出MakeFile文件&#xff0c;并且CMake是一个跨平台的构建系统&#xff0c;对于复杂的、跨平台的项目&#xff0c;CMake可能是一个更好的解决方案。 CMake的脚本文件是在CMakeLis…

DM8_dminit初始化工具介绍及使用

dminit介绍&#xff1a; dminit 是 DM 数据库初始化工具。在安装 DM 的过程中&#xff0c;用户可以选择是否创建初始 数据库。如果当时没有创建&#xff0c;那么在安装完成之后&#xff0c;可以利用创建数据库工具 dminit 来创建。可以利用 dminit 工具提供的各种参数&#xff…

smb配置,详细图文及配置

samba :网络文件共享服务 ​ Samba是一个能让Linux系统应用Microsoft网络通讯协议的软件&#xff0c;而SMB是Server Message Block的缩写&#xff0c;即为服务器消息块&#xff0c;SMB主要是作为Microsoft的网络通讯协议&#xff0c;后来Samba将SMB通信协议应用到了Linux系统上…

我们为什么还要学习Altium Designer?

Altium Designe&#xff08;简称“AD”&#xff09;是电子设计领域中备受推崇的软件工具之一&#xff0c;拥有强大的功能和灵活的设计环境&#xff0c;也是要用最广泛的EDA工具之一&#xff0c;为电子工程师提供了无限可能&#xff0c;但很多工程师学完AD基本操作就转投其他EDA…

elementUI,自定义表头,多层级表头,表头合并,行内容一致的合并行

先上效果&#xff1a; 1.自定义表头&#xff1a; 通过设置 slot"header" 来自定义表头。 slot-scope"scope" 这一行千万不要因为没有再template中使用到scope&#xff0c;vscode报红而删除&#xff0c;这会导致input框不能输入任何内容&#xff01; &l…