面试官:实现一个 LRU 缓存。巧用 Map 【JavaScript】

前言

大家好,我是 PakJeon.

这是一道比较常见的面试算法题。其实原理上并不难理解,但是实现细节却有很多注意的地方,特别是常规的哈希表+双向链表的实现。这次介绍一个巧妙的方法,巧用 JavaScript 内置的 Map。

什么是LRU

LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,是一种常见的缓存算法,用于管理缓存中的数据。LRU算法的核心思想是保留最近被访问过的数据,而淘汰最久未被访问的数据。

它的工作原理如下:

  1. 初始化: 缓存被初始化为一定的容量,当缓存达到容量上限时,需要淘汰数据。
  2. 访问数据: 每次访问(读取或写入)缓存中的数据时,将该数据移动到数据结构的头部或更新其位置,表示这个数据是最近被使用的。
  3. 淘汰数据: 当缓存达到容量上限时,需要淘汰最久未被访问的数据,通常是数据结构的尾部的数据。
js 复制代码
// 题目需要实现一个 LRUCache,实现以下效果,且 put、get 操作要求是时间复杂度 O(1)

const cache = new LRUCache(2); // 创建一个容量为2的 LRU 实例
console.log(cache.put(1, 1)); // 返回 null   此时cache为【1】
console.log(cache.put(2, 2)); // 返回 null   此时cache为【2,1】
console.log(cache.get(1)); // 返回 1         此时cache为【1,2】,因为使用了1,1变成"新鲜的"
console.log(cache.put(3, 3)); // 返回 null   此时cache为【3,1】,超过容量了,把"老油条"2淘汰
console.log(cache.get(2)); // 返回 -1        此时cache为【3,1】
console.log(cache.put(4, 4)); // 返回 null   此时cache为【4,3】,超过容量了,把"老油条"1淘汰
console.log(cache.get(1)); // 返回 -1        此时cache为【4,3】
console.log(cache.get(3)); // 返回 3         此时cache为【3,4】
console.log(cache.get(4)); // 返回 4         此时cache为【4,3】

JavaScript 中的 Map

JavaScript 中的 Map 是一种数据结构,用于存储键值对,并且不同于普通的对象,Map 的键可以是任意数据类型。前端同学对这个都不陌生,平常用到的setgetdeletesize等 Api 相信不用再多介绍。

这次,我们利用的是Map.prototype.keys,来解决这道算法题。

Map.prototype.keys

我们看看这个 Api 运行的结果是什么。

首先创建一个名为cache的 Map 实例,并设置了三个键值对,分别是firstsecondthird

然后调用chache.keys(),我们得到了一个MapIterator。顾名思义,"Map 迭代器",它是一个迭代器,也是本次介绍解法的重点。

关于迭代器,不熟悉的同学可以参考阮一峰老师的 ECMAScript6 入门中关于iterator的介绍

MapIterator

我们知道,迭代器iteratornext方法,每次调用都会返回一个包含valuedone属性的对象。value指的是这次迭代得到的值(在MapIterator这里, value具体指的就是每个键值对的key),而done是个布尔值,指的是元素是否全部迭代完毕。

不难发现,next方法返回的顺序,正是我们往cache中设置键值对的顺序,这就是解决问题的关键。

利用 MapIterator

既然MapIterator.next()会返回第一个设置的键值对,那刚好就是我们 LRU 缓存中的老油条 。再加上Mapgetset也都是时间复杂度O(1),完美符合我们的需求。

LRU 框架

js 复制代码
var LRUCache = function(capacity) {
    this.cap = capacity;
    this.cache = new Map();
};

LRUCache.prototype.get = function(key) {
    // 待实现
    // 分两种情况:
    // ①:cache中不存在key,返回-1
    // ②:cache中已存在key,返回对应的值,并把它变成"新鲜的"

};

LRUCache.prototype.put = function(key, val) {
    // 待实现
    // 分两种情况:
    // ①:cache中已存在key,把key的值用val覆盖,并把它变成"新鲜的";
    // ②:cache中不存在key,再分两种情况:
    // ②-①:cache的缓存区未满,则直接设置键值对;
    // ②-②:cache的缓冲区已满,把"老油条"干掉,再设置键值对。
};

// 辅助方法,把参数 key 的值变成"新鲜的"
LRUCache.prototype.makeRecently = function(key) {
    // 待实现
};

LRU 实现

有了上面的框架 + MapIterator的知识,剩下的就是往框架里填代码了。

js 复制代码
var LRUCache = function(capacity) {
    this.cap = capacity;
    this.cache = new Map();
};

LRUCache.prototype.get = function(key) {
    // ①:cache中不存在key,返回-1
    if (!this.cache.has(key)) {
        return -1;
    }
    // ②:cache中已存在key,返回对应的值,并把它变成"新鲜的"
    this.makeRecently(key);
    return this.cache.get(key);
};

LRUCache.prototype.put = function(key, val) {
    // ①:cache中已存在key,把key的值用val覆盖,并把它变成"新鲜的";
    if (this.cache.has(key)) {
        this.cache.set(key, value);
        this.makeRecently(key);
        return;
    }
    // ②:cache中不存在key,再分两种情况:
    // ②-①:cache的缓存区未满,则直接设置键值对;
    // ②-②:cache的缓冲区已满,把"老油条"干掉,再设置键值对。
    if (this.cache.size >= this.cap) {
        const oldestKey = this.cache.keys().next().value; // 利用MapIterator获取"老油条"
        this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
};

// 辅助方法,把参数 key 的值变成"新鲜的"
LRUCache.prototype.makeRecently = function(key) {
    const val = this.cache.get(key); // 用变量缓存新鲜值
    this.cache.delete(key); // 删除后再set,就会变成MapIterator的最后一项,也就是"最新鲜的"
    this.cache.set(key, val);
};

结语

希望这篇文章为你提供了对MapLRU缓存算法的深入了解,同时也能够激发你在实际项目中灵活运用这些知识的创造力。如果你有任何问题或想要深入探讨相关主题,欢迎在评论区留言,让我们共同进步!

相关推荐
似水流年流不尽思念1 分钟前
MySQL 的 MVCC 到底解决了幻读问题没有?请举例说明。
mysql·面试
cxyll12341 分钟前
从接口自动化测试框架设计到开发(三)主流程封装、返回数据写入excel
前端·python·excel
就是帅我不改1 分钟前
SpringBoot多租户架构设计终极指南:5种方案彻底解决企业级SaaS隔离难题
后端·面试·架构
D11_2 分钟前
【React】JSX基础
前端·react.js·前端框架
Ynov21 分钟前
本地存储(Local Storage)与Cookie的深度对比
前端·javascript
卓伊凡23 分钟前
复杂项目即时通讯从android 5升级android x后遗症之解决报错#6 java.net.SocketException Software caused
前端·后端
鹏程十八少23 分钟前
4. Android FrameWork之 Binder总结,深入剖析 Android Binder:从 Java 到内核的完整通信图谱
前端
PineappleCoder25 分钟前
同源策略是啥?浏览器为啥拦我的跨域请求?(一)
前端·后端·node.js
遂心_26 分钟前
深入理解 requestAnimationFrame:打造流畅动画的利器
前端·javascript
晴空雨26 分钟前
Zustand vs Redux Toolkit:现代 React 状态管理深度对比
前端·react.js