LRUCache

135 阅读2分钟

LRUCache是一个比较典型的面试题,不管是前端面试还是后台面试,很大概率都会遇到,这里给出一个基本实现。

问题

  1. 实现一个缓存,缓存有一个容量大小为capacity,缓存数据以key/value形式存储。
  2. 当缓存的数据超过capacity的时候,将按照LRU(Least Recent Used, 最近最少使用)规则进行删除,从而腾出空间用于存储新的key/value数据;
  3. 缓存实现get和set操作,并且平均时间复杂度为O(1)

实现原理

  1. 用一个Map类型来存储缓存数据的key/value,其中的key为需要存储的key,但value需要封装下,这里为一个链表节点(没错,需要用到双向链表);
  2. 用双向链表保存所的key/value数据结构。其中,链表头保存的是最近最少访问的节点(即LRU节点),链表尾保存的是最近最常访问的节点;
  3. 如果是get操作,则通过Map,获取对应的链表节点,同时将该节点移动到链表尾部(最近最常访问);
  4. 如果是put操作,则判断是否当前达到了capacity。如果达到了capacity,则首先移除链头节点,然后再在链尾插入新节点;如果没有达到capacity,则直接在链尾插入新节点。

具体实现

定义双向链表节点

class LRUNode {
  key: number;
  value: number;
  next: LRUNode | null;
  prev: LRUNode | null;
  constructor(key: number, value: number) {
    this.key = key;
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

定义LRUCache类

基本结构如下

class LRUCache {
  capacity: number;
  cache: Map<number, LRUNode>;
  head: LRUNode | null;
  tail: LRUNode | null;
  constructor(capacity: number) {
    this.capacity = capacity;
    this.cache = new Map<number, LRUNode>();
    this.head = this.tail = null;
  }
  
  get(key: number): number {
  }

  put(key: number, value: number): void {
  }
}

实现链表的基本操作

1. 实现删除节点

class LRUCache {
  // 其他代码
  ...
  
  // 删除节点
  _remove(node: LRUNode): void {
    const preNode = node.prev;
    const nextNode = node.next;
    if (preNode) {
      preNode.next = nextNode;
    }
    if (nextNode) {
      nextNode.prev = preNode;
    }
    
    // 如果删除的node为head
    if (node === this.head) {
      this.head = node.next;
      this.head && (this.head.prev = null);
    }
    // 如果删除的node为tail
    if (node === this.tail) {
      this.tail = node.prev;
      this.tail && (this.tail.next = null);
    }
  }
}

2. 添加节点到尾部

class LRUCache {
  // 其他代码
  ...
  
  // 添加节点到尾部
  _addToTail(node: LRUNode): void {
    if (!this.tail) {
      this.head = node;
      this.tail = node;
    } else {
      this.tail.next = node;
      node.prev = this.tail;
      this.tail = node;
      this.tail.next = null;
    }
  }
}

3. 移动节点到尾部

class LRUCache {
  // 其他代码
  ...
  
  // 移动节点到尾部
  _moveToTail(node: LRUNode): void {
    this._remove(node);
    this._addToTail(node);
  }
}

实现get操作

class LRUCache {
  // 其他代码
  ...

  get(key: number): number {
    if (this.cache.has(key)) {
      // 存在对应的key
      const node: LRUNode = this.cache.get(key)!;
      this._moveToTail(node);
      return node.value;
    } else {
      // 不存在对应的key
      return -1;
    }
  }
}

实现put操作

class LRUCache {
  // 其他代码
  ...
  
  put(key: number, value: number): void {
    if (this.cache.has(key)) {
      /**
       * 存在对应的key
       * 1. 更新对应节点的value
       * 2. 将节点移动到链表尾部
       */
      const node: LRUNode = this.cache.get(key)!;
      node.value = value;
      this._moveToTail(node);
    } else {
      const node: LRUNode = new LRUNode(key, value);
      if (this.cache.size < this.capacity) {
        /**
         * 容量没有超过capacity
         * 直接将新节点添加到链表尾部
         */
        this._addToTail(node);
      } else {
        /**
         * 容量操作capacity
         * 1. 删除头部节点
         * 2. 将新节点添加到链表尾部
         * 3. 删除cache中对应的key
         */
        const headNode = this.head!;
        this._remove(headNode);
        this._addToTail(node);
        this.cache.delete(headNode.key);
      }
      this.cache.set(key, node);
    }
  }
}

代码

github.com/wdskuki/js-…