为什么需要缓存?
在现代软件系统中,性能优化往往依赖于高效的缓存策略。缓存的作用是存储近期使用的数据,以便快速访问,减少对底层存储或计算的重复操作。然而,缓存的空间通常有限,当缓存达到容量上限时,我们必须决定哪些数据需要被替换。
什么是LRU缓存?
LRU(Least Recently Used)缓存是一种经典的缓存替换策略,它的核心思想是:总是淘汰最久未使用的数据。通过这种方式,LRU 缓存能够保证那些近期访问频率高的数据保留在缓存中,从而最大化缓存命中率。
我将以力扣146. LRU 缓存为例,进行讲解。语言typescript。
LRU缓存实现的明确需求
LRU 缓存无非就两个主要操作需求:
- 通过关键词快速查找元素。
- 维护访问顺序,当缓存达到容量上限我们要有能力删除最早的缓存。
第一种做法(哈希表 + 双向链表)
分析
- 为了保证查找的时间复杂度为O(1),我们肯定不能使用数组(查找需要遍历),我们采取哈希表负责快速查找元素,用双向链表负责维护访问顺序。查找和维护顺序两个功能分离给两个数据结构。
- 为了直观展示数据结构,以下算法使用
typescript。
第一步:创建双向链表
- 为了避免与力扣平台自带的数据结构进行冲突,命名为
MyListNode - 模仿题目,构造
MyListNode数据结构
class MyListNode {
key: number; // 关键词
value: number; // 值
prev: MyListNode | null; // 指向上一个访问更早(旧)的缓存节点
next: MyListNode | null; // 指向上一个访问更新的缓存节点
constructor(key: number = 0, value: number = 0) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
第二步:构造LRUCache的缓存结构
class LRUCache {
capacity: number;
cache: Map<number, MyListNode>;
head: MyListNode;
tail: MyListNode;
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map();
this.head = new MyListNode();
this.tail = new MyListNode();
this.head.next = this.tail; // head.next 指向最旧
this.tail.prev = this.head; // tail.prev 指向最新
}
第三步:构建双向链表的三个函数
新增缓存。
addToTail(node)
- 目前最新的缓存是
tail.prev,把节点放到最后面,记录tail.prev。
进行后续链表修改。
addToTail(node: MyListNode): void {
const tail = this.tail;
const temp = tail.prev;
node.next = tail;
node.prev = temp;
tail.prev = node;
temp.next = node;
}
-
开始时
-
结束时
removeFromList(node)
把
node从缓存中删除。(要删最老的缓存,“最老“的逻辑放主函数)
removeFromList(node: MyListNode): void {
node.next!.prev = node.prev;
node.prev!.next = node.next;
}
moveToTail(node)
将已有的缓存设置为最新缓存。
- 直接使用之前的两个函数
moveToTail(node: MyListNode): void {
this.removeFromList(node);
this.addToTail(node);
}
第四步:主方法逻辑
get(key)
get(key: number): number {
const node = this.cache.get(key); // 获取key对应的节点。有,记为node;没有,undefined
if (!node) return -1;
this.moveToTail(node); // 刚使用,将node设置为最新的缓存
return node.value;
}
put(key, value)
put(key: number, value: number): void {
const cache = this.cache;
let node = cache.get(key);
// 有key对应的node的时候,重新设置value,设置为最新缓存
// 没有的时候,新增一个node,哈希表设置缓存,设置为最新缓存
if (node) {
node.value = value;
this.moveToTail(node);
} else {
node = new MyListNode(key, value);
this.cache.set(key, node);
this.addToTail(node);
}
// 如果超过容量,找到最老的缓存,哈希表删除,双向链表也删除
if (cache.size > this.capacity) {
const oldest = this.head.next!;
this.removeFromList(oldest);
cache.delete(oldest.key)
}
}
(源码)
class MyListNode {
key: number;
value: number;
prev: MyListNode | null;
next: MyListNode | null;
constructor(key: number = 0, value: number = 0) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
class LRUCache {
capacity: number;
cache: Map<number, MyListNode>;
head: MyListNode;
tail: MyListNode;
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map();
this.head = new MyListNode();
this.tail = new MyListNode();
this.head.next = this.tail; // head.next 指向最旧
this.tail.prev = this.head; // tail.prev 指向最新
}
get(key: number): number {
const node = this.cache.get(key);
if (!node) return -1;
this.moveToTail(node);
return node.value;
}
put(key: number, value: number): void {
const cache = this.cache;
let node = cache.get(key);
if (node) {
node.value = value;
this.moveToTail(node);
} else {
node = new MyListNode(key, value);
this.cache.set(key, node);
this.addToTail(node);
}
if (cache.size > this.capacity) {
const oldest = this.head.next!;
this.removeFromList(oldest);
cache.delete(oldest.key)
}
}
addToTail(node: MyListNode): void {
const tail = this.tail;
const temp = tail.prev;
node.next = tail;
node.prev = temp;
tail.prev = node;
temp.next = node;
}
removeFromList(node: MyListNode): void {
node.next!.prev = node.prev;
node.prev!.next = node.next;
}
moveToTail(node: MyListNode): void {
this.removeFromList(node);
this.addToTail(node);
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* var obj = new LRUCache(capacity)
* var param_1 = obj.get(key)
* obj.put(key,value)
*/
第二种做法(哈希表同时负责访问顺序)
- 为了实现LRU缓存,当缓存达到容量上限我们要有能力删除最早的缓存,我们先通过这样的方法:
Map.keys()此时的数据结构为iterator迭代器,遍历所有的关键词key。其核心方法next()就是按照插入的顺序拿下一个值键对,value取值。由此我们便不再需要双向链表,哈希表全程负责关键词快速查找和访问顺序维护。
class LRUCache {
capacity: number;
cache: Map<number, number>;
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map();
}
get(key: number): number {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
// 因为哈希表需要负责访问顺序的维护,需要重新设置。
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
put(key: number, value: number): void {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* var obj = new LRUCache(capacity)
* var param_1 = obj.get(key)
* obj.put(key,value)
*/