本文正在参与掘金团队号上线活动,点击 查看大厂春招职位
一、题目描述
运用你所掌握的数据结构,设计和实现一个LRU (最近最少使用) 缓存机制。
实现 LRUCache 类:
LRUCache(int capacity)以正整数作为容量capacity初始化LRU缓存int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1void 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
二、思路分析
先说说 LRU 算法的概念吧!(题目中的连接也可以看看,就是关于 LRU 的百度百科)
LRU 是 Least Recently Used 的缩写,即最近最少使用,该算法是一种内存管理算法。
这个算法的大概逻辑是:
假设,长期不被使用的数据,在后期被用到的概率相对较低。
因此,当数据的数量达到了一定的阈值的时候,我们可以移除掉最近最少被使用的数据
队列
那么从以上就很容易得知一个特性,那就是 先进先出 ,说到这个 先进先出 我们最先想到的数据结构就是队列 ,举个例子:
// 长度为 2
LRUCache lRUCache = new LRUCache(2);
// 添加一个元素
lRUCache.put(1, 1); | [{ key: 1, value: 1 }]
// 添加第二个元素
lRUCache.put(2, 2); | [{ key: 1, value: 1 }, { key: 2, value: 2 }]
// 添加第三个元素 出队
lRUCache.put(3, 3); | { key: 1, value: 1 } <- [{ key: 2, value: 2 }, { key: 3, value: 3 }, ]
但是有个问题是,队列的 get 和 put 的实现并不满足 O(1) 的时间复杂度
想要在 O(1) 的时间复杂度内完成这两种操作,我们就需要用到特殊的数据结构 哈希链表
哈希链表
什么是哈希链表呢?
简单的说,就是 哈希表 + 双向链表 的形式。
我们都知道 哈希表 是由若干个 键值对(key - value) 组成的。
key key key
----- ----- -----
value value value
但是 哈希表 的 键值对 它是无序的,谁先来谁后来都是一样的。
所以,我们需要一个关系链把它们都串起来,让它们彼此之间不再是毫无关系的。
每个 键值对 它都有一个前驱 键值对 和 后继 键值对 就和双向链表一样。
key -> key -> key
----- ----- -----
value <- value <- value
三、AC 代码
/**
* 链表节点
* @param {number} key
* @param {number} value
*/
const Node = function (key, value) {
this.key = key
this.value = value
this.pre = null // 前驱
this.next = null // 后继
}
/**
* LRU 缓存器
* @param {number} capacity
*/
const LRUCache = function(capacity) {
this.head = null // 链表头
this.end = null // 链表尾
this.capacity = capacity // 容量
this.map = new Map() // 哈希表
};
/**
* 获取指定的记录
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {
const d = this.map.get(key)
// 判断是否存在,不存在返回 -1
if(!d) return -1
// 因为使用了,所有刷新节点
this.refreshNode(d)
return d.value
};
/**
* 添加新的记录
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {
const d = this.map.get(key)
// 当节点不存在的时候
if(!d) {
// 判断是否超出容量,如果超出容量了,就把头部的节点删除
if(this.map.size >= this.capacity) {
// 链表中删除节点并返回 key
const oldKey = this.removeNode(this.head)
// 哈希表中删除节点
this.map.delete(oldKey)
}
// 添加新的记录(节点)
const newNode = new Node(key, value)
this.addNode(newNode)
this.map.set(key, newNode)
}
// 节点存在的时候,更新值,并且刷新节点
else {
d.value = value
this.refreshNode(d)
}
};
/**
* 刷新节点
* @param {Node} node
* @return {void}
*/
LRUCache.prototype.refreshNode = function(node) {
// 当节点再末尾的时候,就不用管它了,已经是最新的了
if(node === this.end) return
// 否则删除掉节点,并重新记录
this.removeNode(node)
this.addNode(node)
};
/**
* 删除节点
* @param {Node} node
* @return {any} key
*/
LRUCache.prototype.removeNode = function(node) {
// 当只有一个节点的时候
if(node === this.head && node === this.end) {
this.head = null
this.end = null
}
// 当节点是尾部的时候
else if (node === this.end) {
this.end = this.end.pre
this.end.next = null
}
// 当节点是头部的时候
else if (node === this.head) {
this.head = this.head.next
this.head.pre = null
}
// 当节点是中间的时候
else {
node.pre.next = node.next
node.next.pre = node.pre
}
// 返回被删除节点的 key
return node.key
};
/**
* 添加节点
* @param {Node} node
* @return {void}
*/
LRUCache.prototype.addNode = function(node) {
// 当末尾节点存在的时候,设置添加节点的前驱是末尾节点,末尾节点的后继是添加节点
if(this.end) {
this.end.next = node
node.pre = this.end
node.next = null
}
// 更换末尾节点
this.end = node
// 当头部节点不存在的时候,将头部节点设置为新增节点
if(!this.head) this.head = node
};
const lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
console.log(lRUCache.get(1)); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
console.log(lRUCache.get(2)); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
console.log(lRUCache.get(1)); // 返回 -1 (未找到)
console.log(lRUCache.get(3)); // 返回 3
console.log(lRUCache.get(4)); // 返回 4
四、总结
使用 队列 来做的话,相对来说比较简单,但是性能方面就会有些不尽人意
所有引出了 哈希链表 ,只要了解 哈希链表 这个数据结构的特点,那么就可以使用该数据结构来解决该问题,并且性能方面相对来说也比较好。
需要注意的就是 添加节点 、删除节点、更新节点 的时候链表的结构,和 head 和 end 的存储,以及及时删减 Map 当中的过时的数据。
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情