LRU 全称是 Least Recently Used,即最近最少使用。LRU 缓存是一种常见的缓存策略,它的主要思想是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也就越小。
在 LRU 缓存策略中,我们将最近使用的数据放在容器的前面,每当有新的数据访问时,我们就将这个新数据放在容器的前面。当容器的大小超过了缓存的最大容量时,我们就删除容器尾部的数据。
我们先根据上面 LRU 的定义和特性,编写一些测试用例
// 测试用例
describe("LRUCache", function () {
let cache;
beforeEach(function () {
cache = new LRUCache(3); // 缓存容量为3
});
it("should return correct value after put", function () {
cache.put("key1", "value1");
expect(cache.get("key1")).toEqual("value1");
});
it("should return -1 for non-existent values", function () {
expect(cache.get("key1")).toEqual(-1);
});
it("should evict least recently used item", function () {
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.put("key4", "value4"); // 此时key1应该被剔除
expect(cache.get("key1")).toEqual(-1);
expect(cache.get("key2")).toEqual("value2");
expect(cache.get("key3")).toEqual("value3");
expect(cache.get("key4")).toEqual("value4");
});
it("should move recently used item to front", function () {
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.get("key1"); // key1被访问,应该被移动到最前
cache.put("key4", "value4"); // 此时key2应该被剔除,因为它是最近最少访问的
expect(cache.get("key1")).toEqual("value1");
expect(cache.get("key2")).toEqual(-1);
expect(cache.get("key3")).toEqual("value3");
expect(cache.get("key4")).toEqual("value4");
});
});
实现 LRU 缓存有多种方式,其中最常见的一种方式是使用双向链表(Double Linked List)和哈希表(Hash Map)。双向链表用于存放所有的数据,其插入、删除的时间复杂度都是 O(1)。哈希表则用于记录每个数据在双向链表中的位置,使得我们能够以 O(1)的时间复杂度找到任何数据。
class ListNode {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.hash = {};
this.count = 0;
this.dummyHead = new ListNode();
this.dummyTail = new ListNode();
this.dummyHead.next = this.dummyTail;
this.dummyTail.prev = this.dummyHead;
}
get(key) {
let node = this.hash[key];
if (node == null) return -1;
this.moveToHead(node);
return node.value;
}
put(key, value) {
let node = this.hash[key];
if (node == null) {
if (this.count == this.capacity) {
this.removeLRUItem();
}
node = new ListNode(key, value);
this.hash[key] = node;
this.addToHead(node);
this.count++;
} else {
node.value = value;
this.moveToHead(node);
}
}
moveToHead(node) {
this.removeFromList(node);
this.addToHead(node);
}
removeFromList(node) {
let tempForPrev = node.prev;
let tempForNext = node.next;
tempForPrev.next = tempForNext;
tempForNext.prev = tempForPrev;
}
addToHead(node) {
node.prev = this.dummyHead;
node.next = this.dummyHead.next;
this.dummyHead.next.prev = node;
this.dummyHead.next = node;
}
removeLRUItem() {
let tail = this.popTail();
delete this.hash[tail.key];
this.count--;
}
popTail() {
let tailItem = this.dummyTail.prev;
this.removeFromList(tailItem);
return tailItem;
}
}
let cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
console.log(cache.get(1)); // 返回 1
cache.put(3, 3); // 该操作会使得关键字 2 作废
console.log(cache.get(2)); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得关键字 1 作废
console.log(cache.get(1)); // 返回 -1 (未找到)
console.log(cache.get(3)); // 返回 3
console.log(cache.get(4)); // 返回 4
在实际应用场景中,我们需要知道如何评价 LRU 的作用,可以通过一个指标来观测。这个指标就是“命中率”。
我们可以在代码中添加一些变量来跟踪缓存的命中次数和请求次数,然后使用这两个变量来计算命中率。
例如,我们可以在 LRUCache 的构造函数中添加两个变量,hits 表示命中次数,requests 表示请求次数。每次调用 get 方法时,我们都将 requests 加 1,如果缓存命中,我们将 hits 加 1。
然后,我们可以添加一个新的方法 getHitRate,返回缓存的命中率。
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
// 添加用于跟踪命中率的变量
this.hits = 0;
this.requests = 0;
}
get(key) {
this.requests++; // 请求次数加1
if (this.map.has(key)) {
this.hits++; // 命中次数加1
let value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
}
return -1;
}
put(key, value) {
if (this.map.has(key)) {
this.map.delete(key);
} else if (this.map.size >= this.capacity) {
this.map.delete(this.map.keys().next().value);
}
this.map.set(key, value);
}
// 添加新的方法,获取命中率
getHitRate() {
return this.requests ? this.hits / this.requests : 0;
}
}
JavaScript 中的 Map 数据结构确实可以用来实现 LRU(Least Recently Used)缓存算法。
使用 Map 的理由在于其内部维护了一个键值对的插入顺序。当一个已存在的键再次被 set 时,这个键会重新移动到末尾。这个特性使得我们可以利用 Map 来表示一种最近最少使用(LRU)的缓存策略。当缓存达到上限后,我们可以简单地删除 Map 中的第一个元素(最早被插入但最久未使用的元素)。
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) {
return -1;
}
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
this.cache.delete(this.cache.keys().next().value);
}
}
}
然而,这种方式的性能并不是最优的,因为每次在 Map 中删除一个元素的操作都需要 O(n)的时间复杂度。如果你需要在性能更加关键的场景中实现 LRU 算法,可能需要配合其他数据结构(例如双向链表)来优化。
在 Messenger(即时通讯应用)中,LRU(最近最少使用)缓存策略可以有多种应用场景:
消息缓存:Messenger 应用通常需要在本地设备上缓存最近的聊天消息,以便用户可以在没有网络连接的情况下查看他们。当设备的存储空间有限时,可以使用 LRU 策略来确定哪些聊天记录需要被删除。最近最少使用的聊天记录(即用户最不可能查看的)将被优先移除。
图片和媒体文件缓存:Messenger 应用中经常会包含图片、音频、视频等媒体文件。这些文件在首次加载后,可以被缓存到本地,以便后面快速加载。然而,这些文件通常会占用大量存储空间。因此,当缓存空间不足时,可以使用 LRU 策略来决定哪些文件被移除,即最近最少使用的文件将被优先删除。
联系人列表缓存:联系人列表和用户信息通常也会被 Messenger 应用缓存。当内存不足时,可以使用 LRU 策略来决定哪些联系人或用户信息被从缓存中删除。
通过使用 LRU 缓存策略,Messenger 应用可以更有效地管理有限的存储空间,同时保证用户最关心的数据(最近和频繁使用的)始终可以被快速访问。