性能优化: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)
 */
相关推荐
careathers6 分钟前
【数据结构】二叉树拓展
数据结构·算法
java_nnnn14 分钟前
LeetCode刷题滑动窗口(长度最小的子数组)
算法·leetcode·职场和发展
@atweiwei17 分钟前
训练 AI 大模型系列之 LoRA:不动模型本体,靠一条低秩旁路改专长(附完整手算算例)
人工智能·算法·ai·语言模型·rag
yi01135 分钟前
DAY27: LeetCode 110. 平衡二叉树 | 在递归中判断是否平衡
人工智能·笔记·python·算法·leetcode·二叉树·递归
9624561 小时前
餐饮 SaaS 优惠券系统架构演进(七)营销定价引擎设计:活动、会员价、优惠券、买赠如何在同一 PriceQuote 中可解释地组合
java·运维·算法
xuxigifxfh1 小时前
牛客:HJ21 简单密码
java·开发语言·算法
qq_408606861 小时前
确界的倒数性质证明
学习·算法
小蒋观天下1 小时前
【安防AI工程实战二】AI摄像头帧率抖动根治:ISP曝光时序与AI推理时序冲突深度定位
人工智能·深度学习·算法·安全·机器学习·ai大模型
云和数据.ChenGuang2 小时前
trace 是RAG检索链路的**全链路追踪日志对象
人工智能·深度学习·算法·排序算法
Evand J2 小时前
【PMSM例程6】永磁同步电机(PMSM)基于增广EKF的d-q轴电感分阶段辨识算法|电机参数估计与误差分析
算法·matlab·电机·ekf