前端需要知道的 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来模拟。哈希表是一种根据键值对来直接访问数据的数据结构,是一种映射关系。

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

相关推荐
进取星辰7 分钟前
10、Context:跨维度传音术——React 19 状态共享
前端·react.js·前端框架
wfsm8 分钟前
react使用01
前端·javascript·react.js
小小小小宇20 分钟前
Vue 3 的批量更新机制
前端
阳光普照世界和平24 分钟前
从单点突破到链式攻击:XSS 的渗透全路径解析
前端·web安全·xss
海码00734 分钟前
【Hot100】 73. 矩阵置零
c++·线性代数·算法·矩阵·hot100
MrsBaek40 分钟前
前端笔记-AJAX
前端·笔记·ajax
前端Hardy43 分钟前
第4课:函数基础——JS的“魔法咒语”
前端·javascript
孟陬1 小时前
如何检测 Network 请求异常 - PerformanceObserver
前端
前端小棒槌1 小时前
vue .sync 和 v-model 区别
前端
飞舞花下1 小时前
el-popover实现下拉滚动刷新
前端·javascript·vue.js