面试必刷:LeetCode146 LRU 缓存|JavaScript 手撕全解

0 阅读3分钟

大家好,我是前端开发者,146. LRU 缓存 - 力扣(LeetCode)JavaScript 手撕完整版


一、题目回顾

设计并实现一个 LRU(最近最少使用)缓存 数据结构:

  • LRUCache(int capacity):初始化容量
  • int get(int key):存在返回值,不存在返回 -1
  • void put(int key, int value):插入 / 更新;满了就淘汰最久未使用
  • 要求:get / put 均为 O(1) 时间复杂度

示例:

输入:["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]

二、核心原理:为什么必须是「Map + 双向链表」?

要满足 O(1) 必须两个结构配合:

  1. 哈希表(Map)

    • 快速查找:key → node
    • 没有它,找节点要 O (n)
  2. 双向链表

    • 记录访问顺序
    • 头部 = 最近使用
    • 尾部 = 最久未使用(要删就删它)
    • 支持 O (1) 删除、移动、插入头部

image.png 为什么不用单向链表?因为删除节点需要前驱节点,单向链表要遍历找前驱 → O (n)。


三、结构设计

1. Node 节点结构

function Node(key, value) {
  this.key = key;       // 存key!删除时要用来清map
  this.value = value;
  this.prev = null;     // 刚创建,前后没人
  this.next = null;
}
  • prev/next 初始 null:表示还没连接到链表
  • 节点必须存 key:删除尾节点时,才能去 map 里删掉对应 key

2. LRUCache 构造函数(this 到底是谁)

var LRUCache = function(capacity) {
  this.capacity = capacity;   // 最大容量
  this.map = new Map();       // key → Node(O(1)查找)

  // 虚拟头尾节点(占位,永不删除)
  this.head = new Node(0, 0);
  this.tail = new Node(0, 0);

  // 初始链表:head <-> tail
  this.head.next = this.tail;
  this.tail.prev = this.head;
};

这里的 this = 你 new 出来的缓存实例

const cache = new LRUCache(2);
// cache 就是 this
  • this.map:每个缓存自己独立的哈希表
  • 虚拟头尾:避免空链表、边界判断,所有真实节点插中间

四、4 个工具方法(链表操作)

1. 添加到头部(最近使用)

image.png

LRUCache.prototype.addToHead = function(node) {
  node.prev = this.head;
  node.next = this.head.next;

  this.head.next.prev = node;
  this.head.next = node;
};

2. 删除任意节点

image.png

LRUCache.prototype.removeNode = function(node) {
  let prev = node.prev;
  let next = node.next;
  prev.next = next;
  next.prev = prev;
};

3. 移到头部(get / 更新时调用)

LRUCache.prototype.moveToHead = function(node) {
  this.removeNode(node);
  this.addToHead(node);
};

4. 删除尾部(最久未使用)

LRUCache.prototype.removeTail = function() {
  let tailNode = this.tail.prev;
  this.removeNode(tailNode);
  return tailNode;
};

五、核心 API 实现

1. get 方法(取值)

LRUCache.prototype.get = function(key) {
  // 不存在返回-1
  if (!this.map.has(key)) return -1;

  // 取到节点
  let node = this.map.get(key);
  // 移到头部 = 标记最近使用
  this.moveToHead(node);

  return node.value;
};
  • this.map:当前缓存实例自己的哈希表
  • this:谁调用 getthis 就是谁

2. put 方法(存值 / 更新)

LRUCache.prototype.put = function(key, value) {
  if (this.map.has(key)) {
    // 已存在:更新 + 移到头部
    let node = this.map.get(key);
    node.value = value;
    this.moveToHead(node);
  } else {
    // 不存在:新建节点
    let newNode = new Node(key, value);
    this.map.set(key, newNode);
    this.addToHead(newNode);

    // 超容量:删最久未使用(尾部)
    if (this.map.size > this.capacity) {
      let tailNode = this.removeTail();
      this.map.delete(tailNode.key); // 同步清map
    }
  }
};

六、完整可提交代码(LeetCode 直接过)

function Node(key, value) {
  this.key = key;
  this.value = value;
  this.prev = null;
  this.next = null;
}

var LRUCache = function(capacity) {
  this.capacity = capacity;
  this.map = new Map();

  this.head = new Node(0, 0);
  this.tail = new Node(0, 0);
  this.head.next = this.tail;
  this.tail.prev = this.head;
};

LRUCache.prototype.addToHead = function(node) {
  node.prev = this.head;
  node.next = this.head.next;
  this.head.next.prev = node;
  this.head.next = node;
};

LRUCache.prototype.removeNode = function(node) {
  let prev = node.prev;
  let next = node.next;
  prev.next = next;
  next.prev = prev;
};

LRUCache.prototype.moveToHead = function(node) {
  this.removeNode(node);
  this.addToHead(node);
};

LRUCache.prototype.removeTail = function() {
  let tailNode = this.tail.prev;
  this.removeNode(tailNode);
  return tailNode;
};

LRUCache.prototype.get = function(key) {
  if (!this.map.has(key)) return -1;
  let node = this.map.get(key);
  this.moveToHead(node);
  return node.value;
};

LRUCache.prototype.put = function(key, value) {
  if (this.map.has(key)) {
    let node = this.map.get(key);
    node.value = value;
    this.moveToHead(node);
  } else {
    let newNode = new Node(key, value);
    this.map.set(key, newNode);
    this.addToHead(newNode);
    if (this.map.size > this.capacity) {
      let tailNode = this.removeTail();
      this.map.delete(tailNode.key);
    }
  }
};

七、高频面试问答

  1. 为什么用 Map? O (1) 查找 key 对应的 Node。
  2. 为什么用双向链表? O (1) 删除、移动、插入,不需要找前驱。
  3. Node 里为什么要存 key? 删除尾节点时,要拿到 key 去清 map。
  4. 虚拟头尾有什么用? 不用判断空链表、边界条件,代码更干净。
  5. this 到底是谁? new LRUCache() 出来的实例对象
  6. 为什么 map 存 node 不存 value? 要操作链表(移动、删除)必须拿到整个节点。

八、总结

LRU 本质就一句话:哈希表保证快查,双向链表保证顺序,两者配合实现 O (1) 缓存淘汰。