LeetCodeLRU缓存 - 最近最少使用缓存机制 - JavaScript描述 - Map - 双向链表

Posted YK菌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCodeLRU缓存 - 最近最少使用缓存机制 - JavaScript描述 - Map - 双向链表相关的知识,希望对你有一定的参考价值。


嗨!~ 大家好,我是YK菌 🐷 ,一个微系前端 ✨,爱思考,爱总结,爱记录,爱分享 🏹,欢迎关注我呀 😘 ~ [微信号: yk2012yk2012,微信公众号:ykyk2012]

先说说我与这道题的缘分吧~ 第一次是去哪儿的一面,面试官问我知道LRU缓存吗,让我实现一个javascript版本的… 我说我听过,但是我可能不太会实现,然后就给我换成简单题反转链表了。虽然写出来简单题了,但是反手还是给我了~ 第二次遇到是快手二面,面试官问我知道LRU缓存吗? 我说我知道一点,然后他说你不知道没事,我告诉你,就给我解释了一下什么是LRU,然后告诉我我要实现什么功能,然后在面试官的指导下我就给做出来了~ 当然最后这轮面试也通过了~

146. LRU 缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间

示例

  • 输入
    ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]

[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]

  • 输出
    [null, null, null, 1, null, -1, null, -1, 3, 4]

  • 解释

1. LRUCache lRUCache = new LRUCache(2);
2. lRUCache.put(1, 1); // 缓存是 1=1
3. lRUCache.put(2, 2); // 缓存是 1=1, 2=2
4. lRUCache.get(1);    // 返回 1
5. lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 1=1, 3=3
6. lRUCache.get(2);    // 返回 -1 (未找到)
7. lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 4=4, 3=3
8. lRUCache.get(1);    // 返回 -1 (未找到)
9. lRUCache.get(3);    // 返回 3
10. lRUCache.get(4);    // 返回 4

分析一下,实现最近最少使用缓存机制。这里有两个操作,一个是get读数据,一个是put写数据或更新数据。这个缓存的容量是有限的,也就是说数据写(put)多了会删掉的数据,何为的数据呢,就是最久没有被读过(get)或者更新(put)的数据,换句话说,读过或者更新过的数据会变

接下来就是要选用哪一种数据结构了,我们可以想象如果这些数据从来没有被读取过,那这是不是就相当于是一个队列

然后就是如果get读取了数据,数据会变,体现出来的就是移动到队列的最右侧

好,下面仔细分析读取和写入操作

get读取数据 : 在缓存中查找位置,找到的话,将这个数据移动到最右侧并返回该数据;没有找到的话,返回-1

put写入数据 : 在缓存中查找位置,有的话,执行更新操作,更新值并将这个数据移动到最右侧; 没有找到的话,执行添加操作,就在最右侧添加新数据;这里添加操作又分两种情况,一是容量还没有满,就直接添加,二是容量已满,就执行删除操作,删除最左侧的数据。

这里涉及的数据的操作有:

查找 涉及到键值对,还有查找,比较容易想到的就是用一个js中的对象来存储键值对

移动 涉及数据频繁移动的数据结构我们想到的就是链表

删除 删除链表一侧的节点,我们想到的就是双向链表

最后我们设计的数据结构应该是这样的

关于双向链表的实现可以参看这篇博文 【LeetCode】设计链表II —— JavaScript实现双向链表 - 掘金 (juejin.cn)

这样,删除、添加、移动数据的操作,都是改变链表的各种指针即可~

【解法一】 双向链表

首先定义一个双向链表中的节点类,节点存储键和值两个数据

class ListNode 
  constructor(key, value) 
    this.key = key
    this.value = value
    this.next = null
    this.prev = null
  

然后开始定义我们的定义 LRU 缓存机制

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操作,获取元素
  get(key) 
    // 直接从对象中获取这个节点
    let node = this.hash[key];
    // 找不到就返回-1
    if (node === null) 
      return -1;
     else 
      // 找到了,就移动节点到链表头部(删除这个节点,然后添加到头部)
      this.removeFromList(node);
      this.addToHead(node);
      // 然后返回此节点的值
      return node.value;
    
  

  put(key, value) 
    // 现在对象中找这个节点
    let node = this.hash[key];
    // 找不到就进行添加操作
    if (node == null) 
      // 如果缓存满了,就删除尾节点
      if (this.count == this.capacity) 
        let tail = this.dummyTail.prev;
        this.removeFromList(tail);
        // 删除对象中的映射关系
        delete this.hash[tail.key];
        // 缓存的数据数量减一
        this.count--;
      
      // 如果缓存没有满,就创建一个新的节点
      let newNode = new ListNode(key, value);
      // 在对象中建立映射
      this.hash[key] = newNode;
      // 添加的链表头部
      this.addToHead(newNode);
      // 缓存的数据数量加一
      this.count++;
     else 
      // 找到了就执行【更新操作】
      node.value = value;
      this.removeFromList(node);
      this.addToHead(node);
    
  

  // 从链表中删除节点的操作
  removeFromList(node) 
    let temp1 = node.prev;
    let temp2 = node.next;
    temp1.next = temp2;
    temp2.prev = temp1;
  

  // 把节点添加到链表头部的操作
  addToHead(node) 
    node.prev = this.dummyHead;
    node.next = this.dummyHead.next;
    this.dummyHead.next.prev = node;
    this.dummyHead.next = node;
  

【解法二】Map

在JavaScript中借助Map这个数据结构里面的一些API,我们可以很容易就实现出一个 LRU 缓存

Map与Object类型的一个主要差异是,Map实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。

keys()和values()分别返回以插入顺序生成键和值的迭代器

如何移动一个元素到顶部呢,和我们上面用双链表实现是一样的逻辑,直接删除这个元素,然后重新插入它

class LRUCache 
  constructor(capacity) 
    this.capacity = capacity;
    this.map = new Map();
  

  get(key) 
    if (this.map.has(key)) 
      let temp = this.map.get(key);
      // 将元素从map中删除
      this.map.delete(key);
      // 然后重新插入到map中
      this.map.set(key, temp);
      return temp;
     else 
      return -1;
    
  

  put(key, value) 
    if (this.map.has(key)) 
      this.map.delete(key);
    
    this.map.set(key, value);
    if (this.map.size > this.capacity) 
      // 删除最“老”的节点,也就是最先插入元素,map.keys产生的是一个迭代器,所以使用next可以获取第一个元素
      this.map.delete(this.map.keys().next().value);
    
  

你猜,我面试的时候写的是哪个版本?

最后,欢迎关注我的专栏,和YK菌做好朋友

以上是关于LeetCodeLRU缓存 - 最近最少使用缓存机制 - JavaScript描述 - Map - 双向链表的主要内容,如果未能解决你的问题,请参考以下文章

LeetCodeLRU缓存 - 最近最少使用缓存机制 - JavaScript描述 - Map - 双向链表

LRU(最近最少使用)缓存机制

Java--缓存热点数据,最近最少使用算法

为啥使用最近最少使用的简单缓存机制?

面试题 16.25. LRU 缓存

Java每日一题——>剑指 Offer II 031. 最近最少使用缓存(分析合适的数据结构过程)