LeetCode热题(JS版) - 146. 实现一个LRU缓存机制

60 阅读3分钟

题目描述

实现一个 LRU (Least Recently Used,最近最少使用) 缓存机制。该缓存应该支持以下操作: 获取数据 get 和写入数据 put。

获取数据 get(key):如果缓存中存在 key,则获取其对应的 value 值;否则返回 -1。

写入数据 put(key, value):如果 key 不存在,则写入其对应的 value 值;如果 key 已经存在,则更新其对应的 value 值,并把它移到最前面。

缓存容量固定且不会超过指定大小,一旦缓存已满,则需要移除最近最少使用的数据,腾出位置给新的数据。

解题思路

由于本题需求中包含了缓存淘汰策略、缓存数据结构的维护及查找,因此我们可以采用 Map + 双向链表(DoublyLinked List) 来实现本题。

  • 使用 Map 存储键值对,以保证在 O(1) 时间内查询元素是否存在
  • 使用 DoublyLinked List 存储元素节点,以便实现快速删除及添加元素,并保证元素访问顺序
  • 当元素数量达到缓存容量时,应该从链表尾部开始淘汰元素

代码实现

class Node {
  public key: number;
  public value: number;
  public prev: Node;
  public next: Node;

  constructor(key:number, value: number, prev?: Node, next?: Node) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

class LRUCache {
  private size: number; // 缓存容量,用于检查是否已经超过容量
  private hashMap: {[key: number]: Node}; // 哈希表,用于快速查找节点
  private left: Node; // 链表头
  private right: Node; // 链表尾

  constructor(size: number) { // 初始化方法
    this.size = size;
    this.hashMap = {}; 
      
    // 创建哑节点(dummy nodes)
    this.left = new Node(0,0);//left.next为Least Recently Used (LRU)
    this.right = new Node(0,0);//right.prev为Most Recently Used
    this.left.next = this.right; // 头节点指向尾节点
    this.right.prev = this.left; // 尾节点指向头节点
  }

  // 从双向链表删除一个节点
  remove(node: Node): void{
    let prev = node.prev;
    let next = node.next;
    prev.next = next;
    next.prev = prev;
  }

  // 在双向链表头部插入节点(该节点将成为最近使用的节点)
  insert(node: Node): void{
    let next = this.left.next; // 旧头结点
    // 新节点插入到“left dummy”和“next”之间
    this.left.next = node;
    next.prev = node;
    // 设置新节点的指针
    node.next = next;
    node.prev = this.left;
  }

  get(key: number): number { // 查询缓存
    if (key in this.hashMap) { // 如果哈希表中存在该键值对
      this.remove(this.hashMap[key]);// 将节点移除
      this.insert(this.hashMap[key]);// 插入到头部,作为最近使用的节点
      return this.hashMap[key].value; // 返回查找到的值
    }
    return -1; // 没有查找到,返回-1
  }

  put(key: number, valueue: number): void { // 添加缓存
    if (key in this.hashMap) { // 如果哈希表中已经存在该键值对
      this.remove(this.hashMap[key]); // 先将旧节点移除
    }
    let newNode = new Node(key, valueue); // 创建新节点
    this.hashMap[key] = newNode; // 在哈希表中添加该键值对
    this.insert(newNode); // 在双向链表头部插入新节点

    if(Object.keys(this.hashMap).length > this.size){ 
      // 检查是否超出缓存容量
      let lru = this.right.prev; // 最近最少使用(LRU)的节点为尾节点的上一个节点
      this.remove(lru); // 将该节点从双向链表删除
      delete this.hashMap[lru.key]; // 从哈希表中删除键值对
    }
  }
}

时间复杂度

put 和 get 操作的时间复杂度均为 O(1)。其中,put 操作中需要在 Map 中查询 key 是否存在,也是 O(1) 的时间复杂度。

空间复杂度

空间复杂度为 O(capacity),需要存储容量大小个元素。