如何在JavaScript中实现LRU缓存

108 阅读7分钟

在JavaScript中实现LRU缓存

缓存在RAM中保存数据。这使得数据的检索比典型的数据库(数据存储在磁盘上)快得多。与磁盘相比,RAM的空间更小。

因此,缓存算法,如最近最少使用(LRU),可以帮助在RAM中失效最近没有使用过的条目。

主要启示

在本文的最后,你将能够理解。

  1. 什么是LRU高速缓存。
  2. 用来构建高效LRU缓存算法的数据结构。
  3. 使用JavaScript实现LRU缓存算法。

前提是

为了能够很好地学习这篇文章,你需要。

  1. 安装[Visual Studio Code]。
  2. 有一个[LeetCode]账户。
  3. 对链表数据结构和哈希表有一定了解。
  • [哈希表]
  • [链表数据结构]

什么是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)。

这在下面的代码例子中会清楚地看到。

我们还可以优化搜索。

搜索可以通过使用一个哈希表来优化。使用哈希表,你将有一个键和一个值对。在这种情况下,键将是我们缓存中的实际值。值将是我们的值的地址,如下图所示。

Hash Table & Double Linked List Diagram

为了将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 作为环境。
  • 类似这样的东西将显示在调试控制台标签上。

Output

结论

在实现LRU缓存时,最好的数据结构是一个双链表和一个哈希表。这个实现的时间复杂度是O(1)。

你也可以自己去尝试一下。