面试官:实现一个 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缓存算法的深入了解,同时也能够激发你在实际项目中灵活运用这些知识的创造力。如果你有任何问题或想要深入探讨相关主题,欢迎在评论区留言,让我们共同进步!

相关推荐
前端之虎陈随易18 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
一路向北he18 小时前
字节钢铁军团--“提供情境,而非控制”
java·开发语言·前端
kyriewen19 小时前
豆包和千问同时关了智能体,我用它们搭的 3 个自动化全废了——迁移方案整理
前端·javascript·ai编程
前端一小卒19 小时前
我用 TypeScript 从零手写了一个 Claude Code,然后发现它的核心只有 30 行
前端·agent
Waay19 小时前
面试口述版:个人对 Prometheus 完整理解
运维·学习·云原生·面试·职场和发展·kubernetes·prometheus
大圣编程20 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang20 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆21 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜1 天前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
负责的蛋挞1 天前
异步HttpModule的实现方式
java·服务器·前端