性能优化:LRU缓存(清晰易懂带图解)

为什么需要缓存?

在现代软件系统中,性能优化往往依赖于高效的缓存策略。缓存的作用是存储近期使用的数据,以便快速访问,减少对底层存储或计算的重复操作。然而,缓存的空间通常有限,当缓存达到容量上限时,我们必须决定哪些数据需要被替换。

什么是LRU缓存?

LRU(Least Recently Used)缓存 是一种经典的缓存替换策略,它的核心思想是:总是淘汰最久未使用的数据。通过这种方式,LRU 缓存能够保证那些近期访问频率高的数据保留在缓存中,从而最大化缓存命中率。

我将以力扣146. LRU 缓存为例,进行讲解。语言typescript

LRU缓存实现的明确需求

LRU 缓存无非就两个主要操作需求:

  1. 通过关键词快速查找元素。
  2. 维护访问顺序,当缓存达到容量上限我们要有能力删除最早的缓存。

第一种做法(哈希表 + 双向链表)

分析

  • 为了保证查找的时间复杂度为O(1),我们肯定不能使用数组(查找需要遍历),我们采取哈希表负责快速查找元素,用双向链表负责维护访问顺序。查找和维护顺序两个功能分离给两个数据结构。
  • 为了直观展示数据结构,以下算法使用typescript

第一步:创建双向链表

  • 为了避免与力扣平台自带的数据结构进行冲突,命名为MyListNode
  • 模仿题目,构造MyListNode数据结构
ts 复制代码
class MyListNode {
  key: number;                // 关键词
  value: number;              // 值
  prev: MyListNode | null;    // 指向上一个访问更早(旧)的缓存节点
  next: MyListNode | null;    // 指向上一个访问更新的缓存节点

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

第二步:构造LRUCache的缓存结构

ts 复制代码
class LRUCache {
  capacity: number;
  cache: Map<number, MyListNode>;
  head: MyListNode;
  tail: MyListNode;

  constructor(capacity: number) {
    this.capacity = capacity;
    this.cache = new Map();
    this.head = new MyListNode();
    this.tail = new MyListNode();
    this.head.next = this.tail; // head.next 指向最旧
    this.tail.prev = this.head; // tail.prev 指向最新
  }

第三步:构建双向链表的三个函数

新增缓存。

addToTail(node)

  • 目前最新的缓存是tail.prev,把节点放到最后面,记录tail.prev
    进行后续链表修改。
ts 复制代码
  addToTail(node: MyListNode): void {
    const tail = this.tail;
    const temp = tail.prev;
    node.next = tail;
    node.prev = temp;
    tail.prev = node;
    temp.next = node;
  }
  • 开始时

  • 结束时

removeFromList(node)

node从缓存中删除。(要删最老的缓存,"最老"的逻辑放主函数)

ts 复制代码
  removeFromList(node: MyListNode): void {
    node.next!.prev = node.prev;
    node.prev!.next = node.next;
  }

moveToTail(node)

将已有的缓存设置为最新缓存。

  • 直接使用之前的两个函数
ts 复制代码
  moveToTail(node: MyListNode): void {
    this.removeFromList(node);
    this.addToTail(node);
  }

第四步:主方法逻辑

get(key)

ts 复制代码
  get(key: number): number {
    const node = this.cache.get(key); // 获取key对应的节点。有,记为node;没有,undefined
    if (!node) return -1;
    this.moveToTail(node); // 刚使用,将node设置为最新的缓存
    return node.value; 
  }

put(key, value)

ts 复制代码
  put(key: number, value: number): void {
    const cache = this.cache;
    let node = cache.get(key);
    // 有key对应的node的时候,重新设置value,设置为最新缓存
    // 没有的时候,新增一个node,哈希表设置缓存,设置为最新缓存
    if (node) {
      node.value = value;
      this.moveToTail(node);
    } else {
      node = new MyListNode(key, value);
      this.cache.set(key, node);
      this.addToTail(node);
    }
    // 如果超过容量,找到最老的缓存,哈希表删除,双向链表也删除
    if (cache.size > this.capacity) {
      const oldest = this.head.next!;
      this.removeFromList(oldest);
      cache.delete(oldest.key)
    }
  }

(源码)

ts 复制代码
class MyListNode {
  key: number;
  value: number;
  prev: MyListNode | null;
  next: MyListNode | null;

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

class LRUCache {
  capacity: number;
  cache: Map<number, MyListNode>;
  head: MyListNode;
  tail: MyListNode;

  constructor(capacity: number) {
    this.capacity = capacity;
    this.cache = new Map();
    this.head = new MyListNode();
    this.tail = new MyListNode();
    this.head.next = this.tail; // head.next 指向最旧
    this.tail.prev = this.head; // tail.prev 指向最新
  }

  get(key: number): number {
    const node = this.cache.get(key);
    if (!node) return -1;
    this.moveToTail(node);
    return node.value;
  }

  put(key: number, value: number): void {
    const cache = this.cache;
    let node = cache.get(key);
    if (node) {
      node.value = value;
      this.moveToTail(node);
    } else {
      node = new MyListNode(key, value);
      this.cache.set(key, node);
      this.addToTail(node);
    }
    if (cache.size > this.capacity) {
      const oldest = this.head.next!;
      this.removeFromList(oldest);
      cache.delete(oldest.key)
    }
  }

  addToTail(node: MyListNode): void {
    const tail = this.tail;
    const temp = tail.prev;
    node.next = tail;
    node.prev = temp;
    tail.prev = node;
    temp.next = node;
  }

  removeFromList(node: MyListNode): void {
    node.next!.prev = node.prev;
    node.prev!.next = node.next;
  }

  moveToTail(node: MyListNode): void {
    this.removeFromList(node);
    this.addToTail(node);
  }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */

第二种做法(哈希表同时负责访问顺序)

  • 为了实现LRU缓存,当缓存达到容量上限我们要有能力删除最早的缓存,我们先通过这样的方法: Map.keys() 此时的数据结构为 iterator 迭代器,遍历所有的关键词key。其核心方法next()就是按照插入的顺序拿下一个值键对,value取值。由此我们便不再需要双向链表,哈希表全程负责关键词快速查找和访问顺序维护。
ts 复制代码
class LRUCache {
  capacity: number;
  cache: Map<number, number>;

  constructor(capacity: number) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key: number): number {
    if (!this.cache.has(key)) return -1;
    const val = this.cache.get(key);
    // 因为哈希表需要负责访问顺序的维护,需要重新设置。
    this.cache.delete(key);
    this.cache.set(key, val);
    return val;
  }

  put(key: number, value: number): void {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    this.cache.set(key, value);

    if (this.cache.size > this.capacity) {
      const oldest = this.cache.keys().next().value;
      this.cache.delete(oldest);
    }
  }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */
相关推荐
CoovallyAIHub4 小时前
CVPR 2026 | MixerCSeg:仅2.05 GFLOPs刷新四大裂缝分割基准!解耦Mamba隐式注意力,CNN+Transformer+Mamba三
深度学习·算法·计算机视觉
CoovallyAIHub4 小时前
YOLO26-Pose 深度解读:端到端架构重新设计,姿态估计凭什么跨代领先?
深度学习·算法·计算机视觉
CoovallyAIHub5 小时前
化工厂气体泄漏怎么用AI检测?30张图3D重建气体泄漏场景——美国国家实验室NeRF新研究
深度学习·算法·计算机视觉
颜酱16 小时前
图的数据结构:从「多叉树」到存储与遍历
javascript·后端·算法
zone773921 小时前
006:RAG 入门-面试官问你,RAG 为什么要切块?
后端·算法·面试
CoovallyAIHub1 天前
OpenClaw 近 2000 个 Skills,为什么没有一个好用的视觉检测工具?
深度学习·算法·计算机视觉
CoovallyAIHub1 天前
CVPR 2026 | 用一句话告诉 AI 分割什么——MedCLIPSeg 让医学图像分割不再需要海量标注
深度学习·算法·计算机视觉
CoovallyAIHub1 天前
Claude Code 突然变成了 66 个专家?这个 5.8k Star 的开源项目,让我重新理解了什么叫"会用 AI"
深度学习·算法·计算机视觉
兆子龙1 天前
前端哨兵模式(Sentinel Pattern):优雅实现无限滚动加载
前端·javascript·算法