【LeetCode】LRU缓存 - 最近最少使用缓存机制 - JavaScript描述 - Map - 双向链表

在这里插入图片描述

嗨!~ 大家好,我是YK菌 🐷 ,一个微系前端 ✨,爱思考,爱总结,爱记录,爱分享 🏹,欢迎关注我呀 😘 ~ [微信公众号: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)的数据,换句话说,读过或者更新过的数据会变

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

image.png

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

image.png

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

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

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

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

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

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

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

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

image.png

关于双向链表的实现可以参看这篇博文 【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);
    }
  }
}

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

image.png

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值