被忽略的Map特性——记住键的原始插入顺序

699 阅读4分钟

昨天在牛客上做一道算法题,设计LRU缓存结构LRU是Least Recently Used的缩写,即最近最少使用,想要了解关于LRU的概念,推荐看这篇文章漫画:什么是LRU算法?

题目描述

设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为 k ,并有如下两个功能

  1. set(key, value):将记录(key, value)插入该结构

  2. get(key):返回key对应的value值

提示:

1.某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的,然后都会刷新缓存。

2.当缓存的大小超过k时,移除最不经常使用的记录。

3.输入一个二维数组与k,二维数组每一维有2个或者3个数字,第1个数字为opt,第2,3个数字为key,value

若opt=1,接下来两个整数key, value,表示set(key, value)
若opt=2,接下来一个整数key,表示get(key),若key未出现过或已被移除,则返回-1
对于每个opt=2,输出一个答案\

4.为了方便区分缓存里key与value,下面说明的缓存里key用""号包裹

示例1

输入:[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
返回值:[1,-1]

说明:
[1,1,1],第一个1表示opt=1,要set(1,1),即将(1,1)插入缓存,缓存是{"1"=1}
[1,2,2],第一个1表示opt=1,要set(2,2),即将(2,2)插入缓存,缓存是{"1"=1,"2"=2}
[1,3,2],第一个1表示opt=1,要set(3,2),即将(3,2)插入缓存,缓存是{"1"=1,"2"=2,"3"=2}
[2,1],第一个2表示opt=2,要get(1),返回是[1],因为get(1)操作,缓存更新,缓存是{"2"=2,"3"=2,"1"=1}
[1,4,4],第一个1表示opt=1,要set(4,4),即将(4,4)插入缓存,但是缓存已经达到最大容量3,移除最不经常使用的{"2"=2},插入{"4"=4},缓存是{"3"=2,"1"=1,"4"=4}
[2,2],第一个2表示opt=2,要get(2),查找不到,返回是[1,-1]     

第一个版本

解题思路:

  1. 用对象cache存储数据,在set的时候不仅存储value,并且存一个timestamp,用来记录操作时间。
  2. 在get的时候,更新key的timestamp,表示其最新的操作时间。
  3. 如果set的时候,发现cache的长度超过k,那么循环cache,将timestamp时间最远的key删除,释放空间存储新增的key
/**
 * lru design
 * @param operators int整型二维数组 the ops
 * @param k int整型 the k
 * @return int整型一维数组
 */
function LRU( operators ,  k ) {
    let cache = {} // 缓存结构
    let res = [] // 输出答案
    for(let i = 0; i < operators.length; i++){
        let [opt, key, value] = operators[i]
        let cacheKeyArr = Object.keys(cache)
        // 当缓存结构的长度超过k时,删除最少使用的key
        if (cacheKeyArr.length > k) {
            let flag = null
            let now = new Date().getTime()
            let diff = 0
            cacheKeyArr.forEach(key => {
                if (now - cache[key].time > diff) {
                    flag = key
                }
            })
            Reflect.deleteProperty(cache, key)
        }
        if (opt === 1) {
            cache[key] = {
                "time": new Date().getTime(),
                "value": value
            }
            
        }
        if (opt === 2) {
            if (cache[key]) {
                // 当key被使用时,更新时间戳信息
                cache[key].time = new Date().getTime()
                res.push(cache[key].value )
            } else {
                res.push(-1)
            }
            
        }
    }
    return res
}
module.exports = {
    LRU : LRU
};

运行结果: 失败(太理想化了,在程序执行这么短的时间里,所有的timestamp都是相等的,根本没法比较近远,哈哈)

Map:能够记住键的原始插入顺序

于是乎,查看题解,其中解法之一就是使用Map。MDN文档中对Map的描述如下:

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。

在此之前,对Map的理解还停在它与对象类似,是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。原来它还具备记住键的原始插入顺序的能力。继续翻看它的API

Map 结构原生提供三个遍历器生成函数和一个遍历方法。

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

解题思路: Map.prototype.keys()返回一个iterator。一旦size超过限度,iterator.next()可以按照顺序获取到最早插入的key。再通过Map.prototype.delete删除该key。

/**
 * lru design
 * @param operators int整型二维数组 the ops
 * @param k int整型 the k
 * @return int整型一维数组
 */
function LRU( operators ,  k ) {
    let hash = new Map()
    let res = []
    for(let i = 0; i < operators.length; i++){
       let [opt, key, value] = operators[i]
       if (opt === 1) {
           // set已经存在的key,那么删除重新插入。
           if(hash.get(key)) {
               hash.delete(key)
           }
           if(hash.size === k) {
               let iterator = hash.keys() // 返回key的遍历器
               let k = iterator.next().value // 获取最早插入的key
               hash.delete(k)
           }
           hash.set(key, value)
       }
       if (opt === 2) {
           if (hash.get(key)) {
               let temp = hash.get(key)
               hash.delete(key)
               hash.set(key, temp) // 重新插入key
               res.push(temp)
           } else {
               res.push(-1)
           }
       }
    }
    return res
}
module.exports = {
    LRU : LRU
};

参考:

  1. 漫画:什么是LRU算法?
  2. # Set 和 Map 数据结构