9、✅ 手写 LRU 缓存淘汰算法(Least Recently Used)

100 阅读2分钟

🎯 一、为什么要掌握 LRU 算法?

  • 前端/全栈/后端通吃的经典算法题

  • 实际应用场景广泛:页面缓存、图片懒加载缓存、React Fiber 中的任务优先队列

  • Map + 双向链表是核心实现方式(JS 可简化)

  • 面试官借此考察你对:

    • 数据结构选择
    • 性能复杂度分析
    • 手写能力的掌握

📌 二、什么是 LRU?

最近最少使用(Least Recently Used)缓存策略: 保持固定容量,最近访问的数据会优先保留,最久未使用的被淘汰。


🎯 三、功能需求(面试通常要求)

  • 固定容量 capacity

  • 支持:

    • get(key):如果存在则返回值,并更新访问顺序
    • put(key, value):如果超出容量,则删除最久未使用项
  • 要求:所有操作时间复杂度 O(1)


✍️ 四、用 JS Map 手写简版(🔥推荐:简洁 + 面试可讲)

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map(); // 保证插入顺序
  }

  get(key) {
    if (!this.cache.has(key)) return -1;

    const value = this.cache.get(key);
    // 先删掉旧位置,再插入到末尾(表示最近使用)
    this.cache.delete(key);
    this.cache.set(key, value);

    return value;
  }

  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key); // 删除旧位置
    } else if (this.cache.size >= this.capacity) {
      // 删除最旧的(Map 的第一个 key)
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }

    this.cache.set(key, value);
  }
}

✅ 五、验证用例

const lru = new LRUCache(2);

lru.put(1, 'A');
lru.put(2, 'B');
console.log(lru.get(1)); // A

lru.put(3, 'C'); // 淘汰 key=2
console.log(lru.get(2)); // -1(不存在)
console.log(lru.get(3)); // C

🧠 六、核心原理图(Map版)

Map 顺序:按访问时间更新(靠后 = 最近使用)

初始:
cache = {1: 'A', 2: 'B'}   → put(3, 'C') 淘汰 1
        ↑ oldest

🔁 七、双向链表版本(深入 + 真正 O(1) 实现)

当面试官要求不能用 Map,你需要自己实现哈希表 + 双向链表

  • 哈希表(对象)存储 key → node 的映射
  • 双向链表维护访问顺序(head=最常用,tail=最少用)
class Node {
  constructor(key, value) {
    this.key = key;
    this.value = value;
    this.prev = this.next = null;
  }
}

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

    // 初始化双向链表的伪头尾节点
    this.head = new Node(null, null);
    this.tail = new Node(null, null);
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  _add(node) {
    node.next = this.head.next;
    node.prev = this.head;
    this.head.next.prev = node;
    this.head.next = node;
  }

  _remove(node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
  }

  get(key) {
    const node = this.map.get(key);
    if (!node) return -1;
    this._remove(node);
    this._add(node);
    return node.value;
  }

  put(key, value) {
    if (this.map.has(key)) {
      this._remove(this.map.get(key));
    }

    const node = new Node(key, value);
    this._add(node);
    this.map.set(key, node);

    if (this.map.size > this.capacity) {
      const lru = this.tail.prev;
      this._remove(lru);
      this.map.delete(lru.key);
    }
  }
}

❗ 八、常见面试陷阱

问题正解
get(key) 要更新顺序吗?是,必须移到最近使用处
超出容量后谁被淘汰?最久未使用(队尾)
为啥时间复杂度是 O(1)?Map + 链表指针插入/删除都是 O(1)
Map 本身会排序吗?是的,按照插入顺序记录(用于简化版)