LRU 缓存机制(题号146)
题目
运用你所掌握的数据结构,设计和实现一个LRU (最近最少使用) 缓存机制 。
实现LRUCache
类:
LRUCache(int capacity)
以正整数作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, int value)
如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:你是否可以在O(1)
时间复杂度内完成这两种操作?
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
提示:
1 <= capacity <= 3000
0 <= key <= 3000
0 <= value <= 104
- 最多调用
3 * 104
次get
和put
链接
解释
这题啊,这题是没想到。
和之前的N题一样,笔者走进了逻辑的死胡同。
这题重要的就是维护LRUCache
的顺序,而且需要注意题目中的最久未使用数据。判断这个需要先了解什么叫使用数据。
这里的使用指的并不只是读取了某个数据,更改也算,新建也算。
那么这就以为这我们在进行查找、修改、新建的时候,都需要把当前数据放到队列的最前面,如果需要删除某条数据,从队列的末尾开始删除即可。
也就是维护一个先出的队列,同时每次进行操作的时候都需要更新队列的数据。
再写这题的时候,笔者总想着利用数组和对象一起操作,这样的想法没错,重点是顺序弄错了。
为了维护一个共同的数据,笔者采用权重分配的方法去对数组进行排序,问题也出现在了权重上,权重的分配总是不均匀的,所以排序总有问题,后来看了别的解答后才发现,原来根本就不需要权重,只需要自己手动维护一个数组即可,也不需要进行排序,每次只需要进行删除和添加到头部的操作即可。
下面的答案都是笔者看完打完后自己手动写出来的,可能会与原答案有些冲突。
自己的答案
无
更好的方法(对象+数组)
上面说了,数组加对象完全可以解决这个问题。数组用来维护顺序,对象用来存储值。
只是这里发现一个奇怪的现象,使用ES5语法时,最后一个用例会超时失败,但换上ES6的class
就成功了,虽然性能时最后的5%。
先看看ES5版本的代码👇:
var LRUCache = function(capacity) {
this.capacity = capacity
this.obj = {}
this.arr = []
};
LRUCache.prototype.get = function(key) {
if (this.arr.findIndex(item => item === key) > -1) {
this.arr = this.arr.filter(item => item !== key)
this.arr.push(key)
return this.obj[key]
} else {
return -1
}
};
LRUCache.prototype.put = function(key, value) {
if (this.arr.findIndex(item => item === key) > -1) {
this.arr = this.arr.filter(item => item !== key)
this.arr.push(key)
this.obj[key] = value
} else {
this.obj[key] = value
this.arr.push(key)
if (this.arr.length > this.capacity) this.arr.shift()
}
};
代码还是比较简单的,初始化时创建一个数组、一个对象,还有就是需要记住数组的最大长度,也就是capacity
。
get
方法的内容也是很简单,首先看看数组中有没有当前元素,如果有就先把它干掉,然后插入到数组末尾中去,最后利用obj
返回对应的值即可;如果没有当前元素,直接返回-1
。
put
方法稍微有点复杂,也是区分当前key
存在和不存在两种情况:
-
当前
key
存在也是老规矩,先改变这个
key
在数组中的位置,把它放到最后。之后改变obj
中的值即可 -
当前
key
不存在不存在的话就是添加了,直接在数组末尾添加
key
即可,同时也需要修改obj
中的值。添加完成后需要判断下数组的长度是否超出限制,如果数组长度超出限制还需要去掉数组第一个元素,直接使用
shift
方法即可。
整体逻辑还是比较简单的,就是性能太差导致最后一个用例没过,现在看看ES6的写法👇:
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.arr = new Array(capacity)
this.obj = {}
}
_hasKey(key) {
return this.arr.findIndex(item => item === key) > -1
}
_deleteKey(key) {
this.arr = this.arr.filter(item => item !== key)
}
get(key) {
if (this._hasKey(key)) {
this._deleteKey(key)
this.arr.push(key)
return this.obj[key]
} else {
return - 1
}
}
put(key, value) {
if (this._hasKey(key)) {
this._deleteKey(key)
this.arr.push(key)
this.obj[key] = value
} else {
this.arr.push(key)
this.obj[key] = value
if (this.arr.length > this.capacity) this.arr.shift()
}
}
}
整体逻辑还是和ES5的差不多,区别就是提取了一些公用的方法,比方说_hasKey
和_deleteKey
,减少了一部分冗余的代码。
改动虽然不大,但也是通过了用例,虽然性能依旧是最后的5%。
更好的方法(Map)
为什么说Map
是更好的方法呢?因为如果使用Map
就不需要使用数组了。因为对象的key
是无法自定义顺序的,所以需要数组来辅助保证顺序。到了Map
这就不需要了,因为Map
的key
是有序的,就是我们插入的顺序。
既然顺序得到了保证,那么只需要对Map
进行相关的增删查操作即可,而且Map
本身自带的set
、delete
、has
、get
操作可以在很大程度上减少代码量,相比对象+数组也是更加方便。
话不多说,直接看代码👇:
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.map = new Map()
}
get(key) {
const value = this.map.get(key)
if (value === undefined) return -1
this.map.delete(key)
this.map.set(key, value)
return value
}
put(key, value) {
if (this.map.has(key)) this.map.delete(key)
this.map.set(key, value)
const keys = this.map.keys()
if (this.map.size > this.capacity) this.map.delete(keys.next().value)
}
}
可以看出来代码量可能只有之前的一半左右,但是功能可一个都不少。
逻辑什么的都差不多,这里不多做缀饰,只是需要注意这里的一个小坑。
看get
方法,该方法利用了Map
的get
方法,get
本身是没有问题的,如果有key
就返回值,没有就返回undefined
。非常合理,主要是这里传入的值有问题,如果这个value
是0
呢?
直接使用:
if (this.map.has(key)) {
...
}
是不行的,因为0
会被判断为false
,笔者当时还困扰了好久,后来仔细思考之后才想出了原因。
所以这里的判断条件只能是value === undefined
。
除了这个别的也就没啥了,和数组+对象差不多。
更好的方法(双向链表)
这个方法就更强了,从逻辑层面上来说简直无敌,就是性能上不如Map
,而且代码实现起来较为复杂。
先说说整体的逻辑,双向链表这个东西的概念就不多少了,之前的文章中有说过到。
这里需要首先给一个头元素和尾元素来保证整条链表的顺利链接,也是为了更加方便的获取头元素和尾元素,因为需要进行相应的插入和删除操作。
但仅仅是双向链表是不够的,因为没在的O(1)
的复杂度上完成查找的工作,所以这里需要另外一个对象或者Map
来直接获取到我们需要的节点,下面的代码中为了方便就使用的对象,使用Map
性能会更好些。
有了对象和双向链表之后就简单了,跟上面的逻辑一样。查找的时候先删除掉当前节点,之后插入到链表的最前面;修改的话也是一样的逻辑,删除+添加;添加的时候就需要先在头节点后面插入新节点,之后去掉尾节点的前一个节点。
看看代码👇:
class DoubleLinkedNode {
constructor(key, value) {
this.key = key
this.value = value
this.prv = null
this.next = null
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.hashMap = {}
this.headNode = new DoubleLinkedNode(null, null)
this.tailNode = new DoubleLinkedNode(null, null)
this.headNode.next = this.tailNode
this.tailNode.prv = this.headNode
}
_isFull() {
return Object.keys(this.hashMap).length >= this.capacity
}
_removeNode(node) {
node.prv.next = node.next
node.next.prv = node.prv
delete this.hashMap[node.key]
return node
}
_addHeadNode(node) {
node.next = this.headNode.next
node.prv = this.headNode
this.headNode.next.prv = node
this.headNode.next = node
this.hashMap[node.key] = node
}
get(key) {
const node = this.hashMap[key]
if (node) {
this._addHeadNode(this._removeNode(node))
return node.value
} else {
return -1
}
}
put(key, value) {
const node = this.hashMap[key]
if (node) {
this._addHeadNode({
...this._removeNode(node),
value
})
} else {
if (this._isFull()) this._removeNode(this.tailNode.prv)
this._addHeadNode(new DoubleLinkedNode(key, value))
}
}
}
代码量果然多了不少呢,先有一个DoubleLinkedNode
类来帮助我们生成新的节点。
之后就是上面说的那些操作,为了避免冗余,提取了_isFull
、_removeNode
和_addHeadNode
三个方法出来,供get
和put
调用。
具体就不一点点细说了,整体逻辑就是这样,和Map的有点类似,区别就是使用数据格式不同。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇