昨天在牛客上做一道算法题,设计LRU缓存结构,LRU是Least Recently Used的缩写,即最近最少使用,想要了解关于LRU的概念,推荐看这篇文章漫画:什么是LRU算法?。
题目描述
设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为 k ,并有如下两个功能
-
set(key, value):将记录(key, value)插入该结构
-
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]
第一个版本
解题思路:
- 用对象cache存储数据,在set的时候不仅存储value,并且存一个timestamp,用来记录操作时间。
- 在get的时候,更新key的timestamp,表示其最新的操作时间。
- 如果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
};