十分钟看懂JS的LRU Cache 算法(下)

上文介绍了LRU Cache的场景点击回顾,以及在es6前提下能够借助Map结构来解决,而本文将介绍在es5条件下,更加根正苗红不取巧的解决方案。javascript

image.png

仍是简单介绍下场景要求,方便没看前一篇的同窗也能直接看(顺便凑点字数):前端

当用户访问不一样站点时,浏览器须要缓存在对应站点的一些信息,当下次访问同一个站点的时候,经过读取缓存就能够实现更快速的访问。缓存的分配空间是有限的,因此当空间不足时,须要优先删除最近不常用的数据,实现缓存的管理。

需求整理

把上述场景问题进一步整理成代码需求:请实现一个class,提供如下功能:java

  1. 提供put方法,用于写入不一样的缓存数据,假设每条数据形式是{'域名','info'},例如{'https://segmentfault.com': '一些关键信息'}(若是是同一域名重复写入,则新写入覆盖旧数据);
  2. 当缓存达到上限时, 调用put写入缓存以前, 要删除最近最少使用的数据
  3. 提供get方法,用于读取缓存数据,同时须要把被读取的数据,移动到最近使用数据 ;
  4. 考虑到性能问题,但愿getset的操做的复杂度是O(1)(简单理解就是,不能使用遍历)

数据选型

一样的,先考虑数据结构的选择,既然是es5那么可选的只有ArrayObject了,array一般用于须要表示顺序的数据结构,可是从上一篇文章咱们能够知道,算法实现的核心,在于维持新插入的数据排在后面,旧数据放在前面的顺序,因此每次读取数据以后须要重排序,来维持这个顺序。而用数组存放数据的话,排序必定躲不开遍历,那就不符合前面的第四条,因此只能考虑Object,然而Object如何表示顺序呢? 就必需要用到链表结构了。node

链表介绍

基本内容

考虑到本篇文章的部分读者有可能第一次接触链表结构,仍是在这里作一下简要的介绍,熟悉的读者能够跳过本节。es6

image.png

链表结构如上图所示,是由一些节点以及节点之间的指针串联起来的。(颜色是为了方便区分属于哪一个节点的指针)面试

  • A`BCD属于常规节点,HeadTail`是虚拟的头部和尾部节点,这是为了方便找到链表的首末设定的;
  • 对于每一个节点而言,它只会记住它的先后位置(若是是单向链表,就只须要记住一个方向;若是是双向链表,就须要分别记住前面和后面的节点,上图是双向链表)并用prenext指针来访问;

Head节点 的preTail节点next都指向null算法

操做图解

仍是以双向链表为例,从末尾增长节点时如图所示,只需改动Tail以及实际末尾节点(本例中是D)的指针便可(关注红色线框标的节点便可):编程

image.png

删除节点则如图(以c节点为例)只须要把该节点先后节点链接,而且把自身的两个指针指向null便可:segmentfault

image.png

移动节点的方式一样借用前文里”移动节点等于先删除后从新从首位插入“这个思路。数组

image.png

如图所示,假设C节点 被get方法读取,那么须要把C节点移到链表最前端,实现从左到右的变化,看似复杂,实际上只要执行如下伪代码:

// 将节点移动到首位
moveToHead(node) {
  if(node){
     // 把B和D连起来
    node.pre.next  = node.next;// B的next指针指向D
    node.next.pre = node.pre; // D的nex指针指向B
    
    // 把C节点移动到head和A之间 
    head.next.pre = node; // A节点的pre指针 指向C
    node.next = head.next; // C节点的next指针 指向A
    node.pre = head;  //  C节点的pre指针 指向head
    head.next = node.pre; // head的next指针 指向C
  }
}

其实就是修改目标节点(C)的先后指针head节点的先后指针,以及目标节点先后节点(B和D)的先后指针(最多就涉及到5个节点,这是固定的,因此复杂度只是O(1))。

上面步骤只是解决了重排序的复杂度问题,可是还须要处理get读取时O(1)复杂度的问题,链表结构方便排序,可是读取难度较大,因此同时咱们还要维护一个hash map(哈希表),在es5下用objec能够实现,那么整个算法的难点基本完成,能够分步写代码了。

算法实现

首先,提供链表节点的类,结构就是domaininfo以外,再加一个pre指针和一个next指针:

function DoubleLinkNode (domain, info){
    this.doamin = domain;
    this.info = info;
    this.pre = null;
    this.next = null;
}

其次,是LRU Cache的构造函数:

function LRUCache (size){
    this.size = size;
    this.hashMap = {};
    // 初始化虚拟的头尾节点 方便找到链表头尾
    this.head = new DoubleLinkNode();
    this.tail = new DoubleLinkNode();
    this.head.next = this.tail;
    this.tail.pre = this.head;
}

而后同样的是先写put方法,

LRUCache.prototype.put = function (domain, info){
   // 首先判断节点是否存在,存在则更新对应信息,不存在则插入
   if(this.hashmap[domain]){
     const node = this.hashmap[domian];
     node.info = info;// 更新
     this.moveToTop(node); // todo1 将节点移动到最前面
     return ;
   } 
   // 不然插入新节点
   const size = Object.keys(this.hashmap).length;
   if(size >= this.size)}{
       // 超过容量,须要先删除最不常用的节点,也就是末尾节点
       const node = this.tail.pre;
       this.removeNode(node); // todo2 将节点移除
   }
   // 正常插入新节点 并添加到最前面
   const newNode =  new DoubleLinkNode(domain, info);
   this.hashMap[domain] = info;
   this.moveToTop(node);
};

为了阅读清晰,能够提取moveToTopdeleteNode方法,接下来补上实现,因为前面说过思路了,也比较简单:

LRUCache.prototype.moveToTop = function (node){
    head.next.pre = node;
    node.next = head.next;
    node.pre = head; 
    head.next = node;
};
LRUCache.prototype.deleteNode = funtion(node) {
    // 链表中移除节点实际上就是将节点的先后节点相连 孤立目标节点便可
    node.prev.next = node.next;
    node.next.prev = node.prev;
    node.prev = null;
    node.next = null; 
    
    // 别忘了还要从哈希表去掉节点的key值
    delete this.hashMap[node.domain];
   
}

最后是get方法, 也比较简单:

LRUCache.prototype.get(domain) = function(){
    if (!this.hashmap[domain]) {
      return false;
    }
    const node = this.hashmap[domain];
    this.deleteNode(node)
    // 由于deleteNode的时候删除了 因此要从新登记
    this.hashmap[domain] = node;
    this.moveToTop(node);
    return node.info;
};

小结

其实双向链表的思路相对前一篇的map实现更有普适性,这个思路不只适用于js,在C语言和其余语言同样能够实现。并且,面试也能够拿来吹牛逼(划重点 面试)。

关于链表结构,有算法基础的同窗可能比较熟悉,可是对于第一次接触的同窗同窗比较陌生,因此考虑再三仍是决定写的详细一些,力求每一篇文章都尽可能让读者读起来不至于太费劲,更贴合本身写博客的初衷。

可能算法类和源码类的文章你们都不是很喜欢(固然也可能仅仅是这类文章我写的不够容易读懂,或者你们以为这是屠龙技,并不实用),相对而言面经和实践知识点的文章更受欢迎,毕竟人都是有畏难情绪的,不过我以为仍是能够对这些内容都作一些了解,学习算法思路,一方面对于找工做的朋友短时间就有帮助,可是对于长期从事编程行业来讲,也可以增加知识,锻炼逻辑能力。

图片

总结

但愿你们对于喜好的文章,可以点赞和收藏,这样也能必定程度上给我个反馈,哪些文章写的较好,哪些文章还有不足,或者对于行文风格和内容有任何意见的,都欢迎私信交流。

最后依然是惯例,RingCentral目前在杭州也设置了办公点,并且能够申请长期远程办公,告别996,工做生活两不误,有兴趣的同窗能够私信咨询(主页有联系方式),能够免费帮忙内推~

相关文章
相关标签/搜索