LRU

LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。

大体流程:

  1. /* 缓存容量为 2 */
  2. LRUCache cache = new LRUCache(2);
  3. // 你可以把 cache 理解成一个队列
  4. // 假设左边是队头,右边是队尾
  5. // 最近使用的排在队头,久未使用的排在队尾
  6. // 圆括号表示键值对 (key, val)
  7. cache.put(1, 1);
  8. // cache = [(1, 1)]
  9. cache.put(2, 2);
  10. // cache = [(2, 2), (1, 1)]
  11. cache.get(1); // 返回 1
  12. // cache = [(1, 1), (2, 2)]
  13. // 解释:因为最近访问了键 1,所以提前至队头
  14. // 返回键 1 对应的值 1
  15. cache.put(3, 3);
  16. // cache = [(3, 3), (1, 1)]
  17. // 解释:缓存容量已满,需要删除内容空出位置
  18. // 优先删除久未使用的数据,也就是队尾的数据
  19. // 然后把新的数据插入队头
  20. cache.get(2); // 返回 -1 (未找到)
  21. // cache = [(3, 3), (1, 1)]
  22. // 解释:cache 中不存在键为 2 的数据
  23. cache.put(1, 4);
  24. // cache = [(1, 4), (3, 3)]
  25. // 解释:键 1 已存在,把原始值 1 覆盖为 4
  26. // 不要忘了也要将键值对提前到队头

分析上面的操作过程,要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:
1、显然 cache 中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。
2、我们要在 cache 中快速找某个 key 是否已存在并得到对应的 val;
3、每次访问 cache 中的某个 key,需要将这个元素变为最近使用的,也就是说 cache 要支持在任意位置快速插入和删除元素。
那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap。

LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体
image.png

借助这个结构,我们来逐一分析上面的 3 个条件:
1、如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。
2、对于某一个 key,我们可以通过哈希表快速定位到链表中的节点,从而取得对应 val。
3、链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 key 快速映射到任意一个链表节点,然后进行插入和删除。
也许读者会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 key,为什么链表中还要存 key 和 val 呢,只存 val 不就行了
「为什么必须要用双向链表」的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。

image.png

var LRUCache = function (capacity) {
    // JS的哈希表结构本身具备LRU性质,即当delete(key)再set(key,val)
    // 则哈希表中key对应的位置被挪到尾部
    this.max = capacity
    this.cache = new Map()
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function (key) {
    const cache = this.cache
    if (cache.has(key)) {
        const value = cache.get(key)
        cache.delete(key)
        cache.set(key, value)
        return value
    } else {
        return -1
    }
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function (key, value) {
    const cache = this.cache
    if (cache.has(key)) cache.delete(key)
    if (cache.size === this.max) cache.delete(cache.keys().next().value)
    cache.set(key, value)
};