前端需要知道的 LRU 算法以及实现

什么是LRU算法

首先要了解的是 LRU 算法是缓存淘汰策略中的经典方案;因为计算机系统中,缓存的容量是有限的,当缓存空间不足的时候,需要根据特定的算法淘汰一些部分数据,LRU算法就是其中之一。

LRU全称:Least Recently Used。直接翻译过来就是最近最少使用 ; 这样理解可能有点模糊,可以理解为最久未使用

从概念中也可以了解到,LRU的核心思想就是:淘汰最久未使用的数据 。我们要实现这个算法, 就要维护一个有序列表,将这些缓存根据访问的时间进行排序,当空间不足的时候,将最后面的数据删除掉。

可以举个例子:

  1. 初始状态:head ↔ tail
  2. 插入A:head ↔ A ↔ tail
  3. 插入B:head ↔ B ↔ A ↔ tail
  4. 访问A:head ↔ A ↔ B ↔ tail
  5. 容量满时插入C:淘汰B → head ↔ C ↔ A ↔ tail

现在我们捋一下,实现LRU要解决的问题:

  • 维护数据使用的顺序
  • 当数据被访问时。有两种情况:
    • 如果数据在缓存中,将它移到头部
    • 如果数据不在缓存中,将它直接添加到头部。并且需要判断缓存是否已经满了,如果满了则需要淘汰
  • 当数据被淘汰时:拿到尾部的数据进行淘汰,

有了思路代码实现起来就方便很多了。

基于Map的简单实现

因为ES6的Map可以保持插入顺序,可以方便地维护访问顺序。

这里最新的数据放在了最后面,最久未使用的放在最前面。

js 复制代码
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity; // 缓存的最大容量
        this.cache = new Map();// 使用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) {
          // 如果不存在,并且内存空间已满,就需要拿到最久未使用的值,将他删除。
            const oldestKey = this.cache.keys().next().value;
            this.cache.delete(oldestKey);
        }
      // 设置被访问的键值对顺序,
        this.cache.set(key, value);
    }
}

基于双向链表+哈希表经典实现

js 复制代码
// 双向链表节点类
class ListNode {
    constructor(key, value) {
        this.key = key;     // 存储键
        this.value = value; // 存储值
        this.prev = null;   // 前指针
        this.next = null;   // 后指针
    }
}

class LRUCache {
    constructor(capacity) {
        this.capacity = capacity; // 缓存容量限制
        this.map = new Map();     // 哈希表,
        
        // 创建虚拟头尾节点,这样跟容易处理再边界的时候
        this.head = new ListNode(-1, -1); // 虚拟头节点
        this.tail = new ListNode(-1, -1); // 虚拟尾节点
        
        // 初始化双向链表关系
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    // 将节点移动到链表头部,表示最近使用
    _moveToHead(node) {
        this._removeNode(node); // 先从链表中移除
        this._addToHead(node);  // 再添加到头部
    }

    // 从链表中移除指定节点
    _removeNode(node) {
        // 修改前后节点的指针,跳过当前节点
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 将节点添加到链表头部
    _addToHead(node) {
        // 将节点插入到虚拟头节点和第一个实际节点之间
        node.next = this.head.next; // 新节点的next指向原第一个节点
        node.prev = this.head;      // 新节点的prev指向虚拟头节点
        
        // 调整原第一个节点的prev指针和新节点的next指针
        this.head.next.prev = node; // 原第一个节点的prev指向新节点
        this.head.next = node;       // 虚拟头节点的next指向新节点
    }

    // 获取缓存中的值
    get(key) {
        if (!this.map.has(key)) return -1; // 键不存在
        
        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 {
            if (this.map.size >= this.capacity) {
                // 缓存已满,需要淘汰最久未使用的节点,也就是尾节点
                const lastNode = this.tail.prev; // 获取虚拟尾节点的前一个节点
                this._removeNode(lastNode);      // 从链表中移除
                this.map.delete(lastNode.key);  // 从哈希表中删除
            }
            // 创建新节点并添加到头部
            const newNode = new ListNode(key, value);
            this.map.set(key, newNode); // 加入哈希表
            this._addToHead(newNode);   // 添加到链表头部
        }
    }
}

看到这里你可能会有疑问,明明第一种实现方案中使用了 Map 就已经实现了,为什么第二种还要添加链表呢?多次一举吗?

这个可能是陷入了一种误区,错把 哈希表Map 划为等号了。 第二种方式,我们只是使用JS中的Map来模拟了哈希表,也可以使用Object来模拟。哈希表是一种根据键值对来直接访问数据的数据结构,是一种映射关系。

所以第二种实现中, 双向链表维护了顺序 ,哈希表维护了

相关推荐
明远湖之鱼21 分钟前
opentype.js 使用与文字渲染
前端·svg·字体
Felven21 分钟前
A. Be Positive
算法
小O的算法实验室25 分钟前
2026年COR SCI2区,自适应K-means和强化学习RL算法+有效疫苗分配问题,深度解析+性能实测,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
青岛少儿编程-王老师1 小时前
CCF编程能力等级认证GESP—C++7级—20250927
数据结构·c++·算法
90后的晨仔1 小时前
Vue 3 组合式函数(Composables)全面解析:从原理到实战
前端·vue.js
今天头发还在吗1 小时前
【React】TimePicker进阶:解决开始时间可大于结束时间的业务场景与禁止自动排版
javascript·react.js·ant design
今天头发还在吗1 小时前
【React】动态SVG连接线实现:图片与按钮的可视化映射
前端·javascript·react.js·typescript·前端框架
小刘不知道叫啥1 小时前
React 源码揭秘 | suspense 和 unwind流程
前端·javascript·react.js
szial1 小时前
为什么 React 推荐 “不可变更新”:深入理解 React 的核心设计理念
前端·react.js·前端框架
夏鹏今天学习了吗1 小时前
【LeetCode热题100(39/100)】对称二叉树
算法·leetcode·职场和发展