当前位置: 首页 > news >正文

(链表:哈希表 + 双向链表)146.LRU 缓存

题目

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

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

实现 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 <= 105
最多调用 2 * 105 次 get 和 put

思路

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表:最近使用的发到链表头,按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
  • 哈希表:为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。作用:快速定位节点,和链表保持数据一致性,确保淘汰过程正确,控制容量

get和put操作流程

get流程:

  1. 判断key是否存在,不存在则返回-1
  2. key存在,难到这个key的节点Node,并将当前这个Node移动到链表的头部
  3. 返回Node结点的value

put流程:

  1. 判断key是否存在哈希表中,不存在,则使用key和calue创建应该新的节点Node,并将这个Node添加到链表的头部,再判断链表中的节点数是否超过容量,超出则删除链表尾部节点Node 和 哈希表中对应的项
  2. 如果key存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

通过分析上面的流程分析,我们需要定义几个数据结构和方法

  • 节点Node的定义:使用双向链表
    • 优点:快速移动到头节点,快速删除尾部节点,维护访问顺序
    • 代码:
      class DLinkedNode {int key;int value;// 前节点DLinkedNode prev;// 后节点DLinkedNode next;// 无参构造public DLinkedNode() {}// 构造public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
      }
      
  • 删除节点:
    private void removeNode(DLinkedNode node) {node.prev.next = node.next;node.next.prev = node.prev;
    }
    
  • 删除尾节点:
private DLinkedNode removeTail() {DLinkedNode res = tail.prev;removeNode(res);return res;
}
  • 新增节点:在头部添加
    private void addToHead(DLinkedNode node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;
    }
  • Node移动到头部的操作:removeToHead
    private void moveToHead(DLinkedNode node) {removeNode(node);addToHead(node);
    }
    
  • LRUCache参数:初始化容量、实时容量、map缓存、头尾节点
    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    // 实时记录缓存元素数量:跟踪当前缓存中的数据量,用于容量控制
    private int size;
    // 容量阈值:定义缓存最大承载量
    private int capacity;
    // head:表操作锚点:作为双向链表的固定起始点,简化头部插入操作
    // tail:LRU节点标识:标记链表末端,便于快速定位待淘汰节点
    private DLinkedNode head, tail;
    
  • 初始化zhegLRUCache
    public LRUCache(int capacity) {this.size = 0;this.capacity = capacity;// 使用伪头部和伪尾部节点head = new DLinkedNode();tail = new DLinkedNode();head.next = tail;tail.prev = head;
    }
    
  • get操作
    public int get(int key) {DLinkedNode node = cache.get(key);if (node == null) {return -1;}// 如果 key 存在,先通过哈希表定位,再移到头部moveToHead(node);return node.value;
    }
    
  • put 操作
    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 存在,先通过哈希表定位,再修改 value,并移到头部node.value = value;moveToHead(node);}
    }
    

算法

public class LRUCache {class DLinkedNode {int key;int value;DLinkedNode prev;DLinkedNode next;public DLinkedNode() {}public DLinkedNode(int _key, int _value) {key = _key; value = _value;}}private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();private 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 存在,先通过哈希表定位,再修改 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;}
}

但是在日常工作中,还是直接使用LinkedHashMap便可以了

  • 插入模式(默认): 新节点追加至链表尾部

    示例插入顺序 A → B → C → D

  • 访问模式

    • accessOrder=true:访问/插入节点均移至尾部,LRU缓存淘汰策略
      • (accessOrder=true): 被访问节点移至链表尾部

        访问B后的顺序 A → C → D → B

    • 默认值:新节点始终追加尾部,保留原始插入顺序
class LRUCache extends LinkedHashMap<Integer, Integer>{private int capacity;public LRUCache(int capacity) {// accessOrder=truesuper(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; }
}
http://www.lqws.cn/news/453295.html

相关文章:

  • XML在线格式化工具
  • MySQL基础多表查询
  • docker安装datax详细步骤
  • AUTOSAR实战教程--OS调试利器ORTI文件使用说明OSEK调试方法
  • OBCP第二章 OceanBase 存储引擎高级技术学习笔记
  • 63 网络交互的过程中目标设备的选择
  • PROFIBUS DP 转 EtherCAT 网关:冶金自动化高效协同的基石
  • 深入剖析HashMap与LinkedHashMap应用
  • 前端页面Javascript数组
  • python之使用cv2.matchTemplate识别缺口滑块验证码---实现最佳图像匹配
  • 主流测距技术深度解析:激光雷达、UWB、微波与视觉方案的全面对比
  • 今日行情明日机会——20250620
  • 响应式数据可视化大屏解决方案,重构工业交互体验
  • 【深度学习基础与概念】笔记(一)深度学习革命
  • 【Golang】go build 命令选项-ldflags用法
  • Spring @ModelAttribute注解全解析:数据绑定与模型管理
  • ceph 通过 crush rule 修改故障域
  • DataWhale-零基础络网爬虫技术(二er数据的解析与提取)
  • LeetCode热题100—— 169. 多数元素
  • leetcode 291. Word Pattern II和290. Word Pattern
  • 解锁数据宝藏:数据挖掘之数据预处理全解析
  • 在Django中把Base64字符串保存为ImageField
  • 思辨场域丨AR技术如何重塑未来学术会议体验?
  • LVS vs Nginx 负载均衡对比:全面解析
  • leetcode-2966.划分数组并满足最大差限制
  • 多相机三维人脸扫描仪:超写实数字人模型制作“加速器”
  • Android Java语言转Kotlin语言学习指导实用攻略
  • 单片机3种按键程序消抖方法
  • DB-GPT启动提示please install by running `pip install cryptography`
  • 函数指针的回调函数与函数跳转执行