LRU 算法
什么是 LRU ?
LRU 全称为 Least Recently Use(最近最少使用),LRU 算法是一种常用的缓存淘汰算法,即选择最近最久未使用的予以淘汰。
大家都知道计算机的缓存容量有限,如果缓存满了就需要删除一些内容,给新的内容腾出位置,但是删除哪些内容比较合适呢?我们希望删除掉那没用的内容,这样当有新的内容进来时还有位置可以进行存储。而 LRU 就是这样的一个淘汰算法策略。我们认为最近刚使用过的内容是有用的,很久没有使用过的内容是无用的,当缓存的容量满了,及内存不够时应该优先删除掉这些很久未使用过的内容。
下面以手机中的应用软件后台管理为例进行说明:
首先将手机后台全部清理掉,第一步先把微信打开,回到主页面之后查看后台顺序可以看到:

第二步再将500px打开,回到主页面之后查看后台可以看到:
此时500px应用在最前面(最右边),微信在最后面(最左边)
第三步同样打开小红书可以看到:
此时小红书在最前面(最右边),微信在最后面(最左边)
第四步同样打开山姆会员可以看到:
此时山姆会员在最前面(最右边),微信在最后面(最左边)
第五步再次打开小红书时可以看到:
此时山姆会员与小红书互换了位置。
经过实验发现:当你越来越多的打开应用时,最新打开的应用都会被排列到后台最前面;当运行内存不够时,后台就会杀掉排列顺序中最左边的应用(即最近未久使用的应用),再将新的应用排列到后台头部。
总结:我们可以总结一下,每当我们打开一个应用时,新的应用都被放入到后台顺序的头部,当打开一个已经在后台运行的应用时,该应用顺序也会被重新放入到头部。当运行内存不够时,后台就会杀掉排列顺序中最左边的应用(即最近未久使用的应用),再将新的应用排列到后台头部。
现在你应该能理解 LRU 缓存淘汰算法了。我们结合 LeetCode 的 第146题 LRU 缓存来讲解一下。
根据题目意思以及结合之前所讲的 LRU 相关知识,我们可以分析该数据结构需要具备什么?
- 每次put数据时,都需要把内容存放到头部,put数据时容量不够时要从尾部删除数据后再将新的数据添加到头部,因此我们需要一个有头有尾且按照顺序保存数据的这样一个数据结构。如下图:
头部是最近使用的数据,越往后就是最久未使用过的数据。
我们可以使用双向链表作为数据结构。单向链表不行吗?
当将数据添加到头部时,双向链表和单向链表没有什么区别,但是当需要删除数据时就有问题了。例如当需要删除给定指针指向的结点时:
我们已经找到了要删除的结点,但是删除某个结点 q 需要知道其前驱结点,而单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表,直到 p->next=q,说明 p 是 q 的前驱结点。
但是对于双向链表来说,这种情况就比较有优势了。因为双向链表中的结点已经保存了前驱结点的指针,不需要像单链表那样遍历。**对于需要删除给定指针指向的结点时:**单链表删除操作需要 O(n) 的时间复杂度,而双向链表只需要在 O(1) 的时间复杂度内就搞定了!
同理,如果我们希望在链表的某个指定结点前面插入一个结点,双向链表比单链表有很大的优势。双向链表可以在 O(1) 时间复杂度搞定,而单向链表需要 O(n) 的时间复杂度。
- 只用双向链表无法实现 O(1) 的get和put操作。
通过前面的分析可知,尽管用双向链表还是无法实现 O(1) 的get和put操作。因为我们不知道需要删除的那个节点在链表中的什么位置,因此需要一个地方去保存它,且时间复杂度为O(1), hash 表可以实现 O(1) 复杂度的get操作,通过将数据保存在 hash 表中,我们可以直接通过 hash 表中获取到的数据获取到前驱节点和后驱节点,从而实现双向链表当需要删除给定指针指向的结点时的 O(1) 时间复杂度的删除操作。
综上所述,我们需要使用 hash 表 + 双向链表来实现 LRU 淘汰算法。其中,链表头部的数据表示最近使用的,尾部数据表示最久未使用的。添加数据时,将其添加到头部;当链表的大小要超出容量时,删除链表尾部的数据,并将新数据添加到头部。当添加的数据链表中存在时,需要更新数据,并将该数据移动到头部去。当使用 get 操作时,需要将该数据添加到头部。
实现
class Node {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
class LRUCache {
constructor(capacity) {
// 初始化头节点、尾节点,辅助用;例如当只有一个节点时,需要删除时就很有用
this.head = new Node(-1, -1);
this.tail = new Node(-1, -1);
this.head.next = this.tail;
this.tail.prev = this.head;
this.capacity = capacity;
this.hashMap = new Map();
}
/**
* 增加节点到头部
* @param {*} node: Node
*/
appendToHead(node) {
const prev = this.head;
const next = this.head.next;
prev.next = node;
node.prev = prev;
node.next = next;
next.prev = node;
}
put(key, value) {
const { hashMap, capacity } = this;
if (hashMap.has(key)) {
const old = hashMap.get(key);
this.remove(old)
this.appendToHead(old);
old.value = value;
} else {
const node = new Node(key, value);
if (hashMap.size >= capacity) {
const removeNode = this.removeLast();
if(removeNode.key !== -1 && removeNode.value !== -1) {
hashMap.delete(removeNode.key);
}
this.appendToHead(node);
hashMap.set(key, node);
} else {
this.appendToHead(node);
hashMap.set(key, node);
}
}
}
get(key) {
const { hashMap } = this;
if (!hashMap.has(key)) {
return -1;
}
// 获取到该 key 对应的节点,删除节点并将其放到链表头部
const node = hashMap.get(key);
this.remove(node);
this.appendToHead(node);
return node.value
}
/**
* 移除指定节点
* @param {*} node: Node
* @returns
*/
remove(node) {
let prev = node.prev;
let next = node.next;
prev.next = next;
next.prev = prev;
return node
}
/**
* 移除最后一个节点
*/
removeLast() {
const prev = this.tail.prev;
if(prev.prev) {
prev.prev.next = this.tail
this.tail.prev = prev.prev
}
return prev;
}
}
// 请你设计并实现一个满足 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) 的平均时间复杂度运行。
var lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
// console.log(lRUCache)
lRUCache.get(1); // 返回 1
// console.log(lRUCache)
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
// console.log(lRUCache)
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
// console.log(lRUCache)
// console.log( lRUCache.get(1) ) // 返回 -1 (未找到)
// console.log( lRUCache ) //
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4