我的编程空间,编程开发者的网络收藏夹
学习永远不晚

JavaScript实现LRU缓存的三种方式详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

JavaScript实现LRU缓存的三种方式详解

LRU全称为Least Recently Used,即最近使用的。针对的是在有限的内存空间内,只缓存最近使用的数据(即get和set的数据),超过有限内存空间的数据将会被删除。这个在面试题中也是常会被问到的内容,接下来就看看怎么来实现。

分析

从定义来看,LRU至少有两个特性:通过键值对读写、有序。实现键值对读写,一般我们会使用哈希表来表示,注意哈希表是一个逻辑结构,实际上我们需要使用objectmap等来实现;数据有序,即最近使用的数据放在前面,已经过时的数据放在后面或被删除,并且支持数据是可排序的,我们可以想到数组、链表、map之类的数据格式。因此,我们有三种方式可以实现LRU缓存:

  • Map
  • Object + Array
  • LinkedList

使用Map实现LRU缓存

Map对象保存的是键值对,并且可以记住键的原始插入顺序。

const map = new Map();
map.set(2, 2);
map.set(1, 2);
console.log(map); // Map(2) {2 => 2, 1 => 2},按照原始的插入顺序
const obj = new Object();
obj[2] = 2;
obj[1] = 1
console.log(obj); // {1: 1, 2: 2},不会按照原始的插入顺序

那么我们就可以根据Map的特性实现LRU,下面是原理图:

实现代码:

class LRUCache {
    data = new Map(); // 数据map
    constructor(length) {
    if (length < 1) throw new Error('长度不能小于1')
        this.length = length
    }

    set(key, value) {
        const data = this.data;
        // 如果存在该对象,则直接删除
        if (data.has(key)) {
            data.delete(key);
        }
        // 将数据对象添加到map中
        data.set(key, value);
        if (data.size > this.length) {
            // 如果map长度超过最大值,则取出map中的第一个元素,然后删除
            const delKey = data.keys().next().value
            data.delete(delKey);
        }
    }
    get(key) {
        const data = this.data;
        // 数据map中没有key对应的数据,则返回null
        if (!data.has(key)) return null;
        const value = data.get(key);
        // 返回数据前,先删除原数据,然后在添加,就可以保持在最新
        data.delete(key);
        data.set(key, value);
        return value;
    }
}

测试一下:

const lruCache = new LRUCache(2);
lruCache.set('1', 1); // Map(1) {1 => 1}
lruCache.set('2',2); // Map(2) {1 => 1, 2 => 2}
console.log(lruCache.get('1')); // Map(2) {2 => 2, 1 => 1}
lruCache.set('3',3); // Map(2) {1 => 1, 3 => 3}
console.log(lruCache.get('2')); // null
lruCache.set('4',4); // Map(2) {3 => 3, 4 => 4}
console.log(lruCache.get('1')); // null
console.log(lruCache.get('3')); // Map(2) {4 => 4, 3 => 3}
console.log(lruCache.get('4')); // Map(2) {3 => 3, 4 => 4}

运行结果:

使用Map基本上就可以实现LRU缓存,并且其兼容性还可以,除了IE兼容性差点:

如果不使用Map,那么还可以使用什么方式实现LRU缓存呢?

使用Object + Array实现LRU缓存

刚刚我们也分析了,LRU需要满足两点:键值对和可排序,这两点可以分别对应到ObjectArray,那么我们是不是就可以以此为思路实现呢?答案是可以的,实现代码:

class LRUCacheObjArr {
    length = 0; // 定义列表最大长度
    dataObj = {}; // 使用对象暂存数据,在可保证get时间复杂度为O(1)
    dataArr = []; // 使用数组解决有序的问题
    constructor(length) {
        if (length < 1) throw new Error('参数非法')
        this.length = length;
    }
    get(key) {
        if (!this.dataObj[key] || !this.dataArr.length) return null;
        // 需要将访问到的值,重新放在数组的最末尾,表示最新的数据
        const index = this.dataArr.findIndex(item => item.key === key);
        // 先删除原数据,然后push到数组末尾
        this.dataArr.splice(index, 1);
        this.dataArr.push(this.dataObj[key]);
        // 返回值,对象是无序的,我们只需要保证里面的数据是最新的即可
        return this.dataObj[key].value;
    }
    set(key, value) {
        // 定义对象数据
        const obj = {key, value};
        const index = this.dataArr.findIndex(item => item.key === key);
        // 判断是否为新增还是修改
        if (index !== -1) {
            // 如果已存在数据,则先删除,然后push到末尾
            this.dataArr.splice(index, 1);
            this.dataArr.push(obj);
        } else {
            // 如果不存在数据,则数组直接push
            this.dataArr.push(obj);
        }
        // 对象新增或者修改同一个对象
        this.dataObj[key] = obj;
        // 判断新增数据后,是否超过最大长度
        if (this.dataArr.length > this.length) {
            // 数组是有序的,超长后直接从头部删除,并且获取删除的数据
            const tmp = this.dataArr.shift();
            // 数据对象里面也应该删除当前删除的对象,避免被再次取到
            delete this.dataObj[tmp.key];
        }
    }
}

我们使用同样的测试案例测试一下:

const lruCache = new LRUCacheObjArr(2);
lruCache.set('1', 1); // dataArr(1) [obj1], dataObj(1) {'1': obj1}
lruCache.set('2',2); // dataArr(2) [obj1,  obj2], dataObj(2) {'1': obj1, '2': obj2}
console.log(lruCache.get('1')); // dataArr(2) [obj2,  obj1], dataObj(2) {'1': obj1, '2': obj2}
lruCache.set('3',3); // dataArr(2) [obj1,  obj3], dataObj(2) {'1': obj1, '3': obj3}
console.log(lruCache.get('2')); // null
lruCache.set('4',4); // dataArr(2) [obj3,  obj4], dataObj(2) {'3': obj3, '4': obj4}
console.log(lruCache.get('1')); // null
console.log(lruCache.get('3')); // dataArr(2) [obj4,  obj3], dataObj(2) {'3': obj3, '4': obj4}
console.log(lruCache.get('4')); // dataArr(2) [obj3,  obj4], dataObj(2) {'3': obj3, '4': obj4}

运行结果:

使用对象+数组的方式,虽然可以实现效果,但是由于会频繁操作数组,因此会牺牲一些性能,而且实现起来也没有Map方便。除了使用数组+对象,其实我们还可以使用双向链表的方式实现LRU。

使用双向链表实现LRU

双向链表,顾名思义就是两个方向的链表,这有别于单向链表。直接看图可能更直观一点:

双向链表在移动元素时,会比单向链表复杂一点,,例如我们想把B和C节点交换一下位置,其过程如下:

这对应到LRU有什么意义呢?双向链表是有序的,每一个节点都知道其上一个或者下一个节点;其存值的方式也是使用键值对的方式,因此完全可以实现LRU。

实现代码:

  class LRUCacheLinked {
      data = {}; // 链表数据
      dataLength = 0; // 链表长度,使用变量保存,可以更快访问
      listHead = null; // 链表头部
      listTail = null; // 链表尾部
      length = 0; // 链表最大长度
      // 构造函数
      constructor(length) {
        if (length < 1) throw new Error('参数不合法')
        this.length = length;
      }
      set(key, value) {
        const curNode = this.data[key];
        if (curNode == null) {
          // 新增数据
          const nodeNew = {key, value};
          // 移动到末尾
          this.moveToTail(nodeNew);
          // 将新增的节点保存到数据对象中,其pre或next将在moveToTail中设置
          this.data[key] = nodeNew;
          // 链表长度递增
          this.dataLength++;
          // 初始化链表头部
          if (this.dataLength === 1) this.listHead = nodeNew;
        } else {
          // 修改现有数据
          curNode.value = value;
          // 移动到末尾
          this.moveToTail(curNode);
        }
        // 添加完数据后可能会导致超出长度,因此尝试着删除数据
        this.tryClean();
      }

      get(key) {
        // 根据key值取出对应的节点
        const curNode = this.data[key];
        if (curNode == null) return null;
        if (this.listTail === curNode) {
          // 如果被访问的元素处于链表末尾,则直接返回值,且不用修改链表
          return curNode.value;
        }
        // 如果是中间元素或者头部元素,则在获取前需要将其移动到链表尾部,表示最新
        this.moveToTail(curNode);
        return curNode.value;
      }
      // 移动节点到链表尾部
      moveToTail(curNode) {
        // 获取链表尾部
        const tail = this.listTail;
        // 如果当前节点就是链表尾部,则不做处理,直接返回
        if (tail === curNode) return;
        // 1. preNode和nextNode断绝与curNode之间的关系
        const preNode = curNode.pre;
        const nextNode = curNode.next;
        if (preNode) {
          if (nextNode) {
            // 当前元素的上一个节点指向其下一个
            preNode.next = nextNode;
          } else {
            // 断开当前元素与上一个节点的联系
            delete preNode.next;
          }
        }
        if (nextNode) {
          if (preNode) {
            // 当前元素的下一个节点指向其上一个
            nextNode.pre = preNode;
          } else {
            // 断开当前元素与下一个节点的关系
            delete nextNode.pre;
          }
          // 如果当前节点是链表头部,则需要重新赋值
          if (this.listHead === curNode) this.listHead = nextNode;
        }

        // 2. curNode断绝与preNode和nextNode之间的关系
        delete curNode.pre
        delete curNode.next

        // 3. 在list末尾,重新建立curNode的新关系
        if (tail) {
          tail.next = curNode;
          curNode.pre = tail;
        }
        // 重新赋值链表尾部,保持最新
        this.listTail = curNode;
      }
      tryClean() {
        while(this.dataLength > this.length) {
          const head = this.listHead;
          if (head == null) throw new Error('链表缺少头部');
          const headNext = head.next;
          if (headNext == null) throw new Error('链表头部缺失下一个节点');
          // 1. 断绝head和headNext之间的关系
          delete head.next;
          delete headNext.pre;
          // 2. 重新赋值listHead
          this.listHead = headNext;
          // 3. 清理data
          delete this.data[head.key];
          // 4. 重新计数
          this.dataLength = this.dataLength - 1;
        }
      }
    }

这么一看,双向链表是这三种方式中最复杂的一个。我们使用同样的案例测试一下:

const lruCache = new LRUCacheLinked(2);
lruCache.set('1', 1);
lruCache.set('2',2);
console.log(lruCache.get('1'));
lruCache.set('3',3);
console.log(lruCache.get('2'));
lruCache.set('4',4);
console.log(lruCache.get('1'));
console.log(lruCache.get('3'));
console.log(lruCache.get('4'));

实现结果:

总结

本文总结了三种实现LRU缓存的方法,其中使用Map是最佳的方式。使用其他两种方式,虽然可以实现效果,但是从效率、可读性上来看,还是Map更胜一筹。这三种方式你都学会了吗?

到此这篇关于JavaScript实现LRU缓存的三种方式详解的文章就介绍到这了,更多相关JavaScript LRU缓存内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

JavaScript实现LRU缓存的三种方式详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java实现LRU缓存的实例详解

Java实现LRU缓存的实例详解1.CacheCache对于代码系统的加速与优化具有极大的作用,对于码农来说是一个很熟悉的概念。可以说,你在内存中new 了一个一段空间(比方说数组,list)存放一些冗余的结果数据,并利用这些数据完成了以空
2023-05-31

Android中Rxjava实现三级缓存的两种方式

本文正如标题所说的用rxjava实现数据的三级缓存分别为内存,磁盘,网络,刚好最近在看Android源码设计模式解析与实战(受里面的ImageLoader的设计启发)。我把代码放到了我的hot项目中,github地址 源码下载地址:Rxja
2022-06-06

JavaScript实现html转pdf的三种方法详解

摘要:本文详细介绍了三种使用JavaScript将HTML转换为PDF的方法:方法一:HTML2Canvas+jsPDF适用于简单文档,跨浏览器兼容性好。方法二:dom-to-image+pdfmake适用于复杂文档,提供更多PDF样式选项。方法三:html2pdf.js专注于HTML转PDF,性能优化,提供丰富的PDF布局选项。选择合适的方法取决于文档复杂度、性能要求和跨浏览器兼容性等因素。
JavaScript实现html转pdf的三种方法详解
2024-04-02

Android Flutter实现搜索的三种方式详解

这篇文章主要为大家详细介绍了Android Flutter实现搜索的三种方式,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的可以了解一下
2022-11-13

JavaScript实现LRU算法的示例详解

不知道屏幕前的朋友们,有没有和我一样,觉得LRU算法原理很容易理解,实现起来却很复杂。所以本文就为大家整理了一下实现的示例代码,需要的可以参考一下
2023-05-17

Go定时器的三种实现方式示例详解

这篇文章主要为大家介绍了Go定时器的三种实现方式示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-12-20

编程热搜

目录