首页 > 其他分享 >LeetCode LRU 缓存

LeetCode LRU 缓存

时间:2024-10-17 16:18:14浏览次数:8  
标签:map 缓存 get value LRU key put LeetCode

题目描述

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

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

函数 getput 必须以 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

题目分析

LRU 算法就是一种缓存淘汰策略,原理不难,但是面试中写出没有 bug 的算法比较有技巧,需要对数据结构进行层层抽象和拆解。

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

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

举个简单的例子,安卓手机都可以把软件放到后台运行,比如我先后打开了「设置」「手机管家」「日历」,那么现在他们在后台排列的顺序是这样的:
在这里插入图片描述
但是这时候如果我访问了一下「设置」界面,那么「设置」就会被提前到第一个,变成这样:
在这里插入图片描述
假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢?

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

首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1

注意哦,getput 方法必须都是 O(1) 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。

/* 缓存容量为 2 */
LRUCache cache = new LRUCache(2);
// 圆括号表示键值对 (key, val)

cache.put(1, 1);
// cache = [(1, 1)]

cache.put(2, 2);
// cache = [(1, 1),(2, 2)]

cache.get(1);       // 返回 1
// cache = [(2, 2),(1, 1)]
// 解释:因为最近访问了键 1,所以重新插入
// 返回键 1 对应的值 1

cache.put(3, 3);
// cache = [(1, 1),(3, 3)]
// 解释:缓存容量已满,需要删除内容空出位置
// 优先删除久未使用的数据,也就是第一个的数据
// 然后把新的数据插入末尾

cache.get(2);       // 返回 -1 (未找到)
// cache = [(1, 1),(3, 3)]
// 解释:cache 中不存在键为 2 的数据

cache.put(1, 4);    
// cache = [(3, 3),(1, 4)]

所以代码思路为

  • map保存key
  • getkey 缓存value 删掉keyset一遍
  • putkey 删掉 重新set 超出内存 删掉第一个key

代码

/**
 * @param {number} capacity
 */
var LRUCache = function(capacity) {
    this.capacity = capacity;
    this.map = new Map();
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
    if(this.map.has(key)){
        let temp=this.map.get(key)
         this.map.delete(key);
         this.map.set(key, temp);
         return temp
    }else{
        return -1
    }
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
    if(this.map.has(key)){
        this.map.delete(key);
    }
    this.map.set(key,value);
    if(this.map.size > this.capacity){
     
        this.map.delete(this.map.keys().next().value);
    }
};

代码分析

LRUCache 构造函数

/**
 * @param {number} capacity
 */
var LRUCache = function(capacity) {
    this.capacity = capacity; // 缓存容量
    this.map = new Map(); // 使用Map来存储key-value对
};
  • capacity 是缓存的最大容量。
  • map 是一个哈希表,用来存储缓存中的键值对。

get 方法

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
    if(this.map.has(key)){
        let temp = this.map.get(key); // 取出key对应的value
        this.map.delete(key); // 删除旧的key-value对
        this.map.set(key, temp); // 重新插入key-value对,这样key就移动到了最近使用的位置
        return temp;
    } else {
        return -1; // 如果key不存在,返回-1
    }
};
  • has(key) 检查缓存中是否有key。
  • get(key) 获取key对应的value。
  • delete(key) 删除旧的key-value对。
  • set(key, value) 插入新的key-value对,这样key就移动到了最近使用的位置。

put 方法

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
    if(this.map.has(key)){
        this.map.delete(key); // 如果key已经存在,先删除旧的key-value对
    }
    this.map.set(key, value); // 插入新的key-value对
    if(this.map.size > this.capacity){
        // 如果当前缓存的容量超过了限制,需要删除最久未使用的key-value对
        // 使用迭代器获取第一个key,然后删除对应的key-value对
        this.map.delete(this.map.keys().next().value);
    }
};
  • has(key) 检查缓存中是否有key。
  • delete(key) 删除旧的key-value对。
  • set(key, value) 插入新的key-value对。
  • size 获取当前缓存的大小。
  • keys() 获取所有的key,然后使用迭代器获取第一个key。
  • delete(key) 删除最久未使用的key-value对。

注意

当我们调用this.map.keys().next().value时,我们实际上是在获取第一个插入Map的键。

get方法中,如果键存在,我们先从Map中删除该键值对,然后重新插入,这实际上将该键值对移动到了最近使用的位置。

put方法中,如果键已经存在,我们先从Map中删除该键值对,然后重新插入新的键值对。如果插入后Map的大小超过了容量,我们会删除第一个键值对,也就是最久未使用的键值对。

所以,这段代码实际上是按照插入顺序来确定最久未使用的键值对的。如果一个键值对在Map中被更新(通过getput方法),它会被移动到Map的末尾,这意味着它被视为最近使用的。

因此,this.map.delete(this.map.keys().next().value);这行代码确实是在删除最久未使用的键值对,因为它是第一个插入的,并且在之前的getput操作中没有被更新过。

代码能够以O(1)的平均时间复杂度运行getput操作,并且正确实现了LRU缓存策略。

案例分析

让我们通过一个具体的例子来分析您提供的LRU缓存代码的工作方式:

假设我们创建了一个容量为2LRU缓存:

var cache = new LRUCache(2);

然后我们进行以下操作:

  1. cache.put(1, 1); 缓存现在包含 {1=1}
  2. cache.put(2, 2); 缓存现在包含 {1=1, 2=2}
  3. cache.get(1); 返回 1,缓存现在变为 {2=2, 1=1},因为1被访问了,所以它被移到了最近使用的位置。
  4. cache.put(3, 3); 缓存满了,需要删除最久未使用的元素,也就是 2,所以缓存变为 {1=1, 3=3}
  5. cache.get(2); 返回 -1,因为 2 已经被删除了。
  6. cache.put(4, 4); 缓存再次满了,删除最久未使用的元素 1,缓存变为 {3=3, 4=4}
  7. cache.get(1); 返回 -1,因为 1 已经被删除了。
  8. cache.get(3); 返回 3,缓存变为 {4=4, 3=3},因为3被访问了,所以它被移到了最近使用的位置。
  9. cache.get(4); 返回 4,缓存变为 {3=3, 4=4},因为4被访问了,所以它被移到了最近使用的位置。

下面是每次操作后缓存的状态:

  1. 初始状态:{}(空缓存)
  2. 插入 1{1=1}
  3. 插入 2{1=1, 2=2}
  4. 访问 1{2=2, 1=1}1 被移到最近使用的位置)
  5. 插入 3{1=1, 3=3}2 被删除)
  6. 访问 2:返回 -12 不存在)
  7. 插入 4{3=3, 4=4}1 被删除)
  8. 访问 1:返回 -11 不存在)
  9. 访问 3{4=4, 3=3}3 被移到最近使用的位置)
  10. 访问 4{3=3, 4=4}4 被移到最近使用的位置)

标签:map,缓存,get,value,LRU,key,put,LeetCode
From: https://blog.csdn.net/weixin_55608297/article/details/143020525

相关文章

  • leetcode 876. Middle of the Linked List
    leetcode876.MiddleoftheLinkedList不容易出错的写法,慢classSolution{public:ListNode*middleNode(ListNode*head){if(!head||!head->next){returnhead;}ListNode*single=head,*double_=head;int......
  • Leetcode 1514. 概率最大的路径
    1.题目基本信息1.1.题目描述给你一个由n个节点(下标从0开始)组成的无向加权图,该图由一个描述边的列表组成,其中edges[i]=[a,b]表示连接节点a和b的一条无向边,且该边遍历成功的概率为succProb[i]。指定两个节点分别作为起点start和终点end,请你找出从起点到终点成......
  • Leetcode 802. 找到最终的安全状态
    1.题目基本信息1.1.题目描述有一个有n个节点的有向图,节点按0到n–1编号。图由一个索引从0开始的2D整数数组graph表示,graph[i]是与节点i相邻的节点的整数数组,这意味着从节点i到graph[i]中的每个节点都有一条边。如果一个节点没有连出的有向边,则该节点是终......
  • 缓存穿透/击穿/雪崩(附生产BUG)
    优质博文:IT-BLOG-CN一、背景为什么要写这篇文章?生产缓存生成服务转java时,需要通过配置文件进行流量切换。开发人员同时打开了两个配置页面。原配置信息=ABCDEF。在第一个配置页面进行缓存切换,添加G业务缓存,配置信息=ABCDEFG。随后H业务也需要进行缓存切换,但开发人员在第......
  • Springboot缓存+定时提交优化频繁数据库表修改
    缘起最近在弄一个答题小程序,当用户选择的时候需要把用户选择的选项提交到服务器上,然后整个字段是个json格式的,更新的方法也就是解析json后添加选项进入json中。于是其中就涉及到频繁的数据库修改,因为用户答题速度根据题目不同嘛,所以我就寻思这样频繁的修改,数据量上来速度就会受......
  • 常见的缓存淘汰算法
    应用场景:缓存淘汰算法可以广泛应用于任何有缓存淘汰需求的场景,并不仅限于某个特定的插件或工具。许多软件和系统,如数据库(Redis、Memcached)、Web服务器(Nginx、Varnish)、内容分发网络(CDN)、浏览器缓存、甚至操作系统的内存管理,都会使用这些算法来决定在缓存空间满时该移除哪些数据......
  • 性能优化实战(三):缓存为王-面向缓存的设计
    如果你觉得这篇文章对你有帮助,请不要吝惜你的“关注”、“点赞”、“评价”、“收藏”,你的支持永远是我前进的动力~~~ 个人收藏的技术大会分享PDF文档,欢迎点击下载查看!!!本文是我在做网易考拉海购性能优化时的真实实践,希望对你也有帮助!!!一、流量走向示意图通常一个请求的......
  • LeetCode第六题:锯齿形转换(Python)
    一.题目要求及实例将给定的字符串,转化为锯齿形。锯齿形的行数给定。按序输出转换后的字符串。二.初始思路(1)二维数组的大小竖着写入二维数组较困难,所以想到了先横着写,再取转置。首先需要知道二维数组的大小。参数中给的numRows即为行数,所以要考虑的就是二维数组的列数。......
  • LeetCode 1884.鸡蛋掉落-两枚鸡蛋:动态规划
    【LetMeFly】1884.鸡蛋掉落-两枚鸡蛋:动态规划力扣题目链接:https://leetcode.cn/problems/egg-drop-with-2-eggs-and-n-floors/给你2 枚相同的鸡蛋,和一栋从第1 层到第n层共有n层楼的建筑。已知存在楼层f,满足 0<=f<=n,任何从高于f的楼层落下的鸡蛋都会......
  • leetcode算法题 437.路径总和
    题目描述给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。 示例示例1:输入:root=[10,5,-3,3,2,null,1......