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

0 阅读4分钟

为什么需要缓存?

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

什么是LRU缓存?

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

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

a9f3c11c37f5cd1bc64d622adde6dd81.png

image.png

LRU缓存实现的明确需求

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

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

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

分析

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

第一步:创建双向链表

  • 为了避免与力扣平台自带的数据结构进行冲突,命名为MyListNode
  • 模仿题目,构造MyListNode数据结构
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的缓存结构

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 指向最新
  }

Pasted image 20260311105638.png

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

新增缓存。

addToTail(node)

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

  • 结束时 Pasted image 20260311110738.png

removeFromList(node)

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

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

Pasted image 20260311111523.png

moveToTail(node)

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

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

第四步:主方法逻辑

get(key)

  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)

  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)
    }
  }

(源码)

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取值。由此我们便不再需要双向链表,哈希表全程负责关键词快速查找和访问顺序维护。
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)
 */