LRU 缓存算法
LRU(Least Recently Used)是一种常见的缓存淘汰策略,用于在缓存容量有限的情况下,优先移除最久未使用的缓存项,从而为新的数据腾出空间。LRU 的基本思想是:当缓存已满且需要淘汰某个缓存项时,优先移除最近最少使用的(即最久未使用的)数据项。
LRU 的核心操作
LRU 缓存算法通常需要支持以下几种操作:
- 获取缓存值(
get(key)) :如果缓存中存在key,则返回对应的值,并且将该缓存项标记为最近使用(提升优先级)。 - 更新或插入缓存值(
put(key, value)) :如果key已经存在,则更新其值并提升其优先级;如果key不存在,则插入新缓存项,如果缓存已满,则移除最久未使用的缓存项。
LRU 算法的实现思路
要实现一个高效的 LRU 缓存,我们通常需要两个数据结构:
- 哈希表(Map) :用于快速查找缓存项。
- 双向链表:用于记录缓存项的访问顺序,最近使用的放在链表头部,最久未使用的放在链表尾部。当缓存满了时,移除链表尾部的元素。
LRU 算法的实现
使用 JavaScript 来实现 LRU 缓存算法,下面的示例使用 Map 和手动管理双向链表来实现:
实现步骤:
get(key):如果key存在于缓存中,则将该缓存项移动到双向链表的头部,并返回值。put(key, value):如果key已经存在,则更新其值并移动到头部。如果key不存在,插入新节点,并检查缓存是否超出容量。如果超出容量,则移除链表尾部的最旧节点。
代码实现:
class LRUCache {
constructor(capacity) {
this.capacity = capacity; // 缓存容量
this.map = new Map(); // 存储缓存的键值对
this.head = null; // 链表头部(最常用)
this.tail = null; // 链表尾部(最不常用)
}
// 双向链表的节点
createNode(key, value) {
return { key, value, prev: null, next: null };
}
// 获取缓存项
get(key) {
if (!this.map.has(key)) {
return -1; // key 不存在
}
const node = this.map.get(key);
// 移动到链表头部
this.moveToHead(node);
return node.value;
}
// 设置缓存项
put(key, value) {
if (this.map.has(key)) {
// 更新已有的节点,并移动到链表头部
const node = this.map.get(key);
node.value = value;
this.moveToHead(node);
} else {
// 创建新节点
const newNode = this.createNode(key, value);
// 添加到链表头部
this.addNodeToHead(newNode);
this.map.set(key, newNode);
// 如果超出容量,移除链表尾部的最少使用项
if (this.map.size > this.capacity) {
this.removeTail();
}
}
}
// 移动节点到链表头部
moveToHead(node) {
if (node === this.head) {
return; // 已经在头部,无需移动
}
// 从当前链表中移除节点
this.removeNode(node);
// 将节点移动到头部
this.addNodeToHead(node);
}
// 将节点添加到链表头部
addNodeToHead(node) {
if (!this.head) {
this.head = this.tail = node;
} else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
// 从链表中移除节点
removeNode(node) {
const { prev, next } = node;
if (prev) {
prev.next = next;
} else {
this.head = next; // 如果 node 是头部,更新 head
}
if (next) {
next.prev = prev;
} else {
this.tail = prev; // 如果 node 是尾部,更新 tail
}
node.prev = node.next = null; // 清除指针
}
// 移除链表尾部节点(最少使用的)
removeTail() {
if (!this.tail) return;
const node = this.tail;
// 从 map 中删除
this.map.delete(node.key);
// 从链表中移除
this.removeNode(node);
}
}
// 使用示例
const cache = new LRUCache(2);
cache.put(1, 1); // 缓存内容为 {1=1}
cache.put(2, 2); // 缓存内容为 {1=1, 2=2}
console.log(cache.get(1)); // 返回 1,缓存内容为 {2=2, 1=1}
cache.put(3, 3); // 缓存容量已满,移除最不常用的键 2,缓存内容为 {1=1, 3=3}
console.log(cache.get(2)); // 返回 -1(未找到)
cache.put(4, 4); // 缓存容量已满,移除最不常用的键 1,缓存内容为 {3=3, 4=4}
console.log(cache.get(1)); // 返回 -1(未找到)
console.log(cache.get(3)); // 返回 3,缓存内容为 {4=4, 3=3}
console.log(cache.get(4)); // 返回 4,缓存内容为 {3=3, 4=4}
解释:
-
put(key, value):- 如果
key存在,更新值并将节点移动到链表头部。 - 如果
key不存在,创建新节点并添加到链表头部。若缓存超出容量,则移除链表尾部的节点(最少使用的缓存项)。
- 如果
-
get(key):- 如果
key存在,返回缓存值并将该节点移动到链表头部。 - 如果
key不存在,返回-1。
- 如果
-
双向链表:
- 双向链表的头部存放最近使用的缓存项,尾部存放最久未使用的缓存项。
- 当缓存容量超出限制时,会移除链表尾部的节点。
时间复杂度分析:
-
get(key)和put(key, value)操作的时间复杂度都是O(1),因为:- 查找
key在Map中的复杂度为O(1)。 - 移动节点到链表头部或移除尾部节点的复杂度也是
O(1)。
- 查找
总结:
- LRU 算法通过结合哈希表和双向链表实现,保证了高效的缓存访问和淘汰策略。
- 哈希表用于快速查找缓存项,双向链表用于记录缓存项的使用顺序,确保最近使用的项在链表头部,最久未使用的项在尾部。