LRU缓存算法

140 阅读4分钟

LRU 缓存算法

LRU(Least Recently Used)是一种常见的缓存淘汰策略,用于在缓存容量有限的情况下,优先移除最久未使用的缓存项,从而为新的数据腾出空间。LRU 的基本思想是:当缓存已满且需要淘汰某个缓存项时,优先移除最近最少使用的(即最久未使用的)数据项。

LRU 的核心操作

LRU 缓存算法通常需要支持以下几种操作:

  1. 获取缓存值(get(key) :如果缓存中存在 key,则返回对应的值,并且将该缓存项标记为最近使用(提升优先级)。
  2. 更新或插入缓存值(put(key, value) :如果 key 已经存在,则更新其值并提升其优先级;如果 key 不存在,则插入新缓存项,如果缓存已满,则移除最久未使用的缓存项。

LRU 算法的实现思路

要实现一个高效的 LRU 缓存,我们通常需要两个数据结构:

  1. 哈希表(Map) :用于快速查找缓存项。
  2. 双向链表:用于记录缓存项的访问顺序,最近使用的放在链表头部,最久未使用的放在链表尾部。当缓存满了时,移除链表尾部的元素。

LRU 算法的实现

使用 JavaScript 来实现 LRU 缓存算法,下面的示例使用 Map 和手动管理双向链表来实现:

实现步骤:

  1. get(key) :如果 key 存在于缓存中,则将该缓存项移动到双向链表的头部,并返回值。
  2. put(key, value) :如果 key 已经存在,则更新其值并移动到头部。如果 key 不存在,插入新节点,并检查缓存是否超出容量。如果超出容量,则移除链表尾部的最旧节点。

代码实现:

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity; // 缓存容量
    this.map = new Map();     // 存储缓存的键值对
    this.head = null;         // 链表头部(最常用)
    this.tail = null;         // 链表尾部(最不常用)
  }

  // 双向链表的节点
  createNode(key, value) {
    return { key, value, prev: null, next: null };
  }

  // 获取缓存项
  get(key) {
    if (!this.map.has(key)) {
      return -1; // key 不存在
    }

    const node = this.map.get(key);
    
    // 移动到链表头部
    this.moveToHead(node);

    return node.value;
  }

  // 设置缓存项
  put(key, value) {
    if (this.map.has(key)) {
      // 更新已有的节点,并移动到链表头部
      const node = this.map.get(key);
      node.value = value;
      this.moveToHead(node);
    } else {
      // 创建新节点
      const newNode = this.createNode(key, value);
      
      // 添加到链表头部
      this.addNodeToHead(newNode);
      this.map.set(key, newNode);

      // 如果超出容量,移除链表尾部的最少使用项
      if (this.map.size > this.capacity) {
        this.removeTail();
      }
    }
  }

  // 移动节点到链表头部
  moveToHead(node) {
    if (node === this.head) {
      return; // 已经在头部,无需移动
    }

    // 从当前链表中移除节点
    this.removeNode(node);

    // 将节点移动到头部
    this.addNodeToHead(node);
  }

  // 将节点添加到链表头部
  addNodeToHead(node) {
    if (!this.head) {
      this.head = this.tail = node;
    } else {
      node.next = this.head;
      this.head.prev = node;
      this.head = node;
    }
  }

  // 从链表中移除节点
  removeNode(node) {
    const { prev, next } = node;

    if (prev) {
      prev.next = next;
    } else {
      this.head = next; // 如果 node 是头部,更新 head
    }

    if (next) {
      next.prev = prev;
    } else {
      this.tail = prev; // 如果 node 是尾部,更新 tail
    }

    node.prev = node.next = null; // 清除指针
  }

  // 移除链表尾部节点(最少使用的)
  removeTail() {
    if (!this.tail) return;

    const node = this.tail;

    // 从 map 中删除
    this.map.delete(node.key);

    // 从链表中移除
    this.removeNode(node);
  }
}

// 使用示例
const cache = new LRUCache(2);

cache.put(1, 1);        // 缓存内容为 {1=1}
cache.put(2, 2);        // 缓存内容为 {1=1, 2=2}
console.log(cache.get(1)); // 返回 1,缓存内容为 {2=2, 1=1}
cache.put(3, 3);        // 缓存容量已满,移除最不常用的键 2,缓存内容为 {1=1, 3=3}
console.log(cache.get(2)); // 返回 -1(未找到)
cache.put(4, 4);        // 缓存容量已满,移除最不常用的键 1,缓存内容为 {3=3, 4=4}
console.log(cache.get(1)); // 返回 -1(未找到)
console.log(cache.get(3)); // 返回 3,缓存内容为 {4=4, 3=3}
console.log(cache.get(4)); // 返回 4,缓存内容为 {3=3, 4=4}

解释:

  1. put(key, value)

    • 如果 key 存在,更新值并将节点移动到链表头部。
    • 如果 key 不存在,创建新节点并添加到链表头部。若缓存超出容量,则移除链表尾部的节点(最少使用的缓存项)。
  2. get(key)

    • 如果 key 存在,返回缓存值并将该节点移动到链表头部。
    • 如果 key 不存在,返回 -1
  3. 双向链表

    • 双向链表的头部存放最近使用的缓存项,尾部存放最久未使用的缓存项。
    • 当缓存容量超出限制时,会移除链表尾部的节点。

时间复杂度分析:

  • get(key)  和 put(key, value)  操作的时间复杂度都是 O(1),因为:

    • 查找 key 在 Map 中的复杂度为 O(1)
    • 移动节点到链表头部或移除尾部节点的复杂度也是 O(1)

总结:

  • LRU 算法通过结合哈希表和双向链表实现,保证了高效的缓存访问和淘汰策略。
  • 哈希表用于快速查找缓存项,双向链表用于记录缓存项的使用顺序,确保最近使用的项在链表头部,最久未使用的项在尾部。