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

前言

大家好,我是 PakJeon.

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

什么是LRU

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

它的工作原理如下:

  1. 初始化: 缓存被初始化为一定的容量,当缓存达到容量上限时,需要淘汰数据。
  2. 访问数据: 每次访问(读取或写入)缓存中的数据时,将该数据移动到数据结构的头部或更新其位置,表示这个数据是最近被使用的。
  3. 淘汰数据: 当缓存达到容量上限时,需要淘汰最久未被访问的数据,通常是数据结构的尾部的数据。
// 题目需要实现一个 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 运行的结果是什么。

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

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

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

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

MapIterator

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

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

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

利用 MapIterator

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

LRU 框架

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的知识,剩下的就是往框架里填代码了。

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

原文链接:https://juejin.cn/post/7317908960757678114 作者:PakJeon

(0)
上一篇 2024年1月1日 上午10:58
下一篇 2024年1月1日 下午4:01

相关推荐

发表回复

登录后才能评论