在JavaScript中实现LRU缓存
缓存在RAM中保存数据。这使得数据的检索比典型的数据库(数据存储在磁盘上)快得多。与磁盘相比,RAM的空间更小。
因此,缓存算法,如最近最少使用(LRU),可以帮助在RAM中失效最近没有使用过的条目。
主要启示
在本文的最后,你将能够理解。
- 什么是LRU高速缓存。
- 用来构建高效LRU缓存算法的数据结构。
- 使用JavaScript实现LRU缓存算法。
前提是
为了能够很好地学习这篇文章,你需要。
- 安装[Visual Studio Code]。
- 有一个[LeetCode]账户。
- 对链表数据结构和哈希表有一定了解。
- [哈希表]
- [链表数据结构]
什么是LRU Cache?
让我们先来了解一下什么是Cache。
缓存是一个存储计算结果的存储器,这样以后查找数据的时候就会更快。它不仅存储计算数据,而且还存储冗余数据。因此,它是一个短期存储存储器。
LRU是指最近使用最少的。它是一种缓存驱逐策略,允许人们识别缓存中哪个项目已经很久没有被使用了。
例如,假设我们有一个大小为4个槽的缓存,我们在缓存中有4个项目:[red, green, white, blue] 。我们想再添加一个项目:black 。
我们将在缓存的开头添加black 。所以现在我们有[black, red, green, white, blue] 。这显示的是访问项目的时间顺序。
最近的项目是black ,其次是red, green, white ,最后是blue 。当我们想把black 加入缓存时,我们发现我们的缓存大小为4,没有足够的空间来加入black ,因为缓存已经满了。
在添加black 到缓存之前,我们需要先驱逐一个项目。这就是我们的LRU驱逐策略发挥作用的地方。
什么是我们最近使用最少的项目?在我们的例子中,是blue ,因为它是我们列表尾部的一个项目。因此,我们需要从缓存中删除blue ,然后添加black 。我们新的缓存顺序将是[black, red, green, white] 。
另一个可以对LRU进行的操作是搜索。使用我们最初的例子,让我们访问第三个项目。red,从我们的缓存中。
我们首先搜索,检查red 是否可用。由于它是可用的,它就成为最近访问的项目。这意味着我们必须将red 移到我们的缓存或列表的前面。我们新的缓存顺序将是[red, black, green, white] 。
通过LRU,我们注意到,当我们访问一个项目时,它就会被移到列表的前面。因此,当我们想删除一个项目时,我们选择最近使用最少的项目:列表末尾的那个,并将其从我们的缓存中删除。
用于建立高效LRU缓存算法的数据结构
从上面的例子中,我们在构建LRU缓存时进行了很多操作。
这些操作包括。
1.搜索
当我们想从我们的列表中获取颜色red ,首先,我们需要检查我们的整个列表,检查该项目是否存在。
如果该元素存在,我们就对列表重新排序,并将red 到列表的开头。在这种情况下,重新排序的时间复杂度将是O(n) 。
时间复杂度是指一个代码块的执行时间。
2.添加项目
当向列表中添加一个项目时,操作的时间复杂度是O(n)。
我们可以通过消除移位操作来优化它。为了做到这一点,我们使用一个双倍喜欢的列表。
仍然使用[red, green, white, blue] 的例子,我们需要添加black 。由于它不在我们的列表中,我们删除了最老的元素blue ,并移位了整个列表中的项目。
在一个双链接的列表中,你需要做的就是删除后面的项目,并更新后面的元素,使其指向前面的元素。然后添加一个新的节点,使其指向第二个元素并更新前面的元素。整个操作的时间复杂度为0(1)。
这在下面的代码例子中会清楚地看到。
我们还可以优化搜索。
搜索可以通过使用一个哈希表来优化。使用哈希表,你将有一个键和一个值对。在这种情况下,键将是我们缓存中的实际值。值将是我们的值的地址,如下图所示。

为了将black 加入到我们的列表中,通过使用双链接列表,我们将从我们的列表中删除后部元素blue 。在这种情况下,Blue 是我们最近访问最少的项目。然后,更新后面的元素,使其指向前面的元素white 。
在我们的哈希表中,我们将删除与我们的键blue 相关的值(地址)。因此,我们的键blue 的值将是哈希表中的null 。
一个新的节点被创建,我们把black 。前面的节点被更新为指向拥有black 的节点。Black 也将有一个与之相关的地址。
在这种情况下,移位是在0(1)的时间复杂度下完成的。
在添加black 之前,我们需要先进行搜索,看看缓存中是否有这个地址。为了以最佳方式搜索黑子,我们去找哈希表。
在哈希表中,键black ,不会有相应的值附加在上面。这意味着它不存在于我们的缓存中。这使得我们可以用O(1)的时间复杂度找出一个项目是否在缓存中。
在JavaScript中实现LRU缓存
- 访问[LeetCode]并登录到你的账户。
- 访问[LRU缓存]问题页面并浏览问题陈述。
我们将使用下面的步骤来实现LRU缓存类。
- 打开visual studio代码,并创建一个新文件。
- 将下面的代码块添加到新文件中。
1.初始化LRU
我们首先用一个正数的容量初始化LRU缓存。
var LRUCache = function (capacity) {
this.capacity = capacity;
this.map = new Map(); // this stores the entire array
// this is boundaries for double linked list
this.head = {};
this.tail = {};
this.head.next = this.tail; // initialize your double linked list
this.tail.prev = this.head;
};
2.获取操作
这个操作将返回键的值,如果它存在,否则,将返回-1。
LRUCache.prototype.get = function (key) {
if (this.map.has(key)) {
// remove elem from current position
let c = this.map.get(key);
c.prev.next = c.next;
c.next.prev = c.prev;
this.tail.prev.next = c; // insert it after last element. Element before tail
c.prev = this.tail.prev; // update c.prev and next pointer
c.next = this.tail;
this.tail.prev = c; // update last element as tail
return c.value;
} else {
return -1; // element does not exist
}
};
3.放置操作
这个操作将更新键的值。
如果找到,将key 和值对添加到缓存中。如果键的数量已经超过了缓冲区的初始化容量,则驱逐最近访问次数最少的项目。
LRUCache.prototype.put = function (key, value) {
if (this.get(key) !== -1) {
// if key does not exist, update last element value
this.tail.prev.value = value;
} else {
// check if map size is at capacity
if (this.map.size === this.capacity) {
//delete item both from map and DLL
this.map.delete(this.head.next.key); // delete first element of list
this.head.next = this.head.next.next; // update first element as next element
this.head.next.prev = this.head;
}
let newNode = {
value,
key,
}; // each node is a hashtable that stores key and value
// when adding a new node, we need to update both map and DLL
this.map.set(key, newNode); // add current node to map
this.tail.prev.next = newNode; // add node to end of the list
newNode.prev = this.tail.prev; // update prev and next pointers of newNode
newNode.next = this.tail;
this.tail.prev = newNode; // update last element
}
};
转到File --> Save As ,将文件保存为lru.js 。
使用下面的例子,我们要用它来帮助我们执行缓存类。
["LRUCache", "put", "put", "get", "put", "get"]
[[2], ['red', 'red'], ['grey', 'grey'], ['red'], ['yellow', 'yellow']]
在put 函数之后添加下面的代码块。
var lRUCache = new LRUCache(2); // capacicity of cache is 2.
lRUCache.put("red", "red"); //cache has {red=red}
lRUCache.put("grey", "grey"); //cache has {red=red, grey=grey}
var param_1 = lRUCache.get("red"); // get's red from the cache
console.log(param_1); // prints the result of the get
lRUCache.put("yellow", "yellow"); // LRU key was grey, evicts key grey, cache has {red=red, yellow=yellow}
var param_2 = lRUCache.get("grey");
console.log(param_2 + " Not found"); // returns -1 (not found)
- 转到
Run --> Start Debugging,然后点击Start Debugging。 - 选择
Node.js作为环境。 - 类似这样的东西将显示在调试控制台标签上。
结论
在实现LRU缓存时,最好的数据结构是一个双链表和一个哈希表。这个实现的时间复杂度是O(1)。
你也可以自己去尝试一下。