「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。
题目
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类:
- LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
- void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 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
-
2、可以通过
get(key)
方法返回对应的值 -
3、可以通过
put(key, value)
存下对应数据 -
4、当调用
put
方法时,如果超出最大存储量capacity
,则需要将最近最少使用
的数据删除
关键点在第4点,什么叫 最近最少使用
,按照题目意思应该为:如果一个数据被使用了,无论是 get
,还是 put
,那么它在所有数据中的排序,应该移动到最前面;那么每次需要删除数据时,应该从排序中,选择排序最尾部的数据。
如果不考虑 get、set
的操作时间复杂度的问题,Js语言中,有一个 Map
的数据结构刚好符合上面的特性:Map
里面的数据是有序的,按照每次插入的顺序向后排列;按照题意,可以实现如下:
-
1、实现
get
:如果缓存中不存在,则返回 -1;如果存在,先获取值value
,再从缓存中删除该key
,再插入该key, value
数据,最后返回value
;这样做的目的,是为了将该数据的位置,刷新到最前面 -
2、实现
push
:如果缓存中出在该key
,先删除;再将数据放入缓存中(为了刷新该数据的位置);最后判断缓存数据的长度,是否大于最大存储量,满足的话,利用Map.keys().next().value
获取对应的最近最少使用
的数据key
,删除该数据
关于 .next()
方法,可以参考 MDN
对于该Api的解释,传送门 ☞
/**
* @param {number} capacity
*/
var LRUCache = function (capacity) {
this.map = new Map();
this.capacity = capacity;
};
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function (key) {
if(this.map.has(key)) {
let value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
} else {
return -1;
}
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function (key, value) {
if(this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
if(this.map.size > this.capacity) {
this.map.delete(this.map.keys().next().value);
}
};
思考: 如果要实现题目要求的 get 和 put 必须以 O(1) 的平均时间复杂度运行,那么就要用到 双向链表 + 哈希表
,为什么:
-
1、对于
get
方法,如果想达到时间复杂度为O(1)
,哈希表
是满足条件的。 -
2、因为存在最大容量的限制,在
put
过程中必然存在超过数量时,要进行删除操作 -
3、对于删除操作:
- 数组结构,插入、删除操作的时间复杂度都为
O(n)
,不可取 - 单向链表,在删除时,需要找到当前节点的前序节点,花费
O(n)
,不可取 - 双向链表,获取前序节点的时间为
O(1)
,删除也只需要将前序节点的下一节点指向当前节点的下一节点即可
- 数组结构,插入、删除操作的时间复杂度都为
具体实现如下:
// 定义双向链表结构
function ListNode(key, value) {
this.key = key;
this.value = value;
this.next = null;
this.prev = null;
}
/**
* @param {number} capacity
*/
var LRUCache = function (capacity) {
this.hash = {}; // 哈希表
this.capacity = capacity;
this.size = 0;
// 默认头、尾节点
this.root = new ListNode('start', 'start');
this.end = new ListNode('end', 'end');
this.root.next = this.end;
this.end.prev = this.root;
// 从链表删除一个节点
this.removeNode = node => {
const prev = node.prev;
const next = node.next;
prev.next = next;
next.prev = prev;
}
// 把节点添加到头部
this.addToHead = node => {
const head = this.root.next;
this.root.next = node;
node.prev = this.root;
node.next = head;
head.prev = node;
}
// 把节点移动到头部
// 先删除,后移动
this.addHead = node => {
this.removeNode(node);
this.addToHead(node);
}
// 移除最后一个节点
this.popEnd = () => {
let node = this.end.prev;
this.removeNode(node);
return node;
}
// 删除缓存的item 数据
this.removeLRUItem = () => {
let node = this.popEnd();
delete this.hash[node.key];
this.size--;
}
};
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function (key) {
const node = this.hash[key];
if(!node) return -1;
this.addHead(node);
return node.value;
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function (key, value) {
let node = this.hash[key];
if(!node) {
if(this.size == this.capacity) {
this.removeLRUItem();
}
node = new ListNode(key, value);
this.addToHead(node);
this.hash[key] = node;
this.size++;
} else {
node.value = value;
this.addHead(node);
}
};
小结
双向链表的特性为,从其任意一个节点开始,可以很方便的获取它的前驱节点和后继节点,所以对于删除操作,双向链表的时间复杂度可以降为 O(1)
LeetCode 👉 HOT 100 👉 LRU 缓存 - 中等题 ✅
合集:LeetCode 👉 HOT 100,有空就会更新,大家多多支持,点个赞👍
如果大家有好的解法,或者发现本文理解不对的地方,欢迎留言评论 😄