认识LRU经典算法

LRU算法系列:

LRU算法是不是听着很耳熟,当然面试经常问到。本文带你了解LRU算法相关经典的实现方式以及为什么采用,至少在面试的时候不至于一问三不知而栽跟头,接下来就看看LRU到底是个啥东西吧。

LRU是什么

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 ——《百度百科》

在计算机中,所有的文件操作都要放在内存中进行,然而计算机内存大小是固定的,所以我们不可能把所有的文件都加载到内存,因此我们需要制定一种策略对加入到内存中的文件进项选择。

常见的页面置换算法有如下几种:

  • OPT 最佳置换算法 (理想中存在的)
  • FIFO 先进先出置换算法
  • LRU 最近最久未使用算法
  • LFU 最少使用置换算法
  • NRU 最近未使用算法

LRU的面向场景

是一种计算机中内存不够的场景下,淘汰旧内容的策略。LRU(Least Recently Used),淘汰掉最不经常使用的。可以稍微多补充两句,因为计算机体系结构中,最大的最可靠的存储是硬盘,它容量很大,并且内容可以固化,但是访问速度很慢,所以需要把使用的内容载入内存中;内存速度很快,但是容量有限,并且断电后内容会丢失,并且为了进一步提升性能,还有CPU内部的 L1 CacheL2 Cache等概念。因为速度越快的地方,它的单位成本越高,容量越小,新的内容不断被载入,旧的内容肯定要被淘汰,所以就有这样的使用背景。

LRU的实现方式

在一般标准的操作系统教材里,会用下面的方式来演示 LRU 原理,假设内存只能容纳3个页大小,按照 7 0 1 2 0 3 0 4 的次序访问页。假设内存按照栈的方式来描述访问时间,在上面的,是最近访问的,在下面的是,最远时间访问的,LRU就是这样工作的。

LRU最简单原理图

但是如果让我们自己设计一个基于 LRU 的缓存,这样设计可能问题很多,这段内存按照访问时间进行了排序,会有大量的内存拷贝操作,所以性能肯定是不能接受的。

那么如何设计一个LRU缓存,使得放入和移除都是 O(1) 的,我们需要把访问次序维护起来,但是不能通过内存中的真实排序来反应,有一种方案就是使用双向链表。

基于HashMap和双向链表实现【经典】

整体的设计思路是,可以使用HashMap<key,value>,key存储双向链表的数值,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。

基于HashMap和双向链表实现原理

其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。

总结如下:

  • 在链表头的是最新使用的。
  • 在尾部的是最旧的,也是下次要清除的。
  • 如果加入的值是链表内存在的则要移动到头部。

HashMap是来配合双向链表,用于减少时间复杂度的。它是可以快速的(O(1)的时间)定位,链表中某个值是否存在。(要不然需要遍历双向链表,时间复杂度为O(n) n为链表长度),定位到某个值存在后能马上获得他的node节点,因为是双向链表,直接用此节点的父节点,指向此节点的子节点(跳出、入栈),在将此节点放到头部就可以了,免除了遍历查找。

Redis的LRU实现

如果按照HashMap和双向链表实现,需要额外的存储存放nextprev指针,牺牲比较大的存储空间,显然是不划算的。所以Redis采用了一个近似的做法,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的。

Redis会基于server.maxmemory_samples配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于严格LRU算法,但是相应消耗也变高,对性能有一定影响,样本值默认为5。

Redis的LRU实现是使用一种近似的算法来模拟LRU淘汰的效果实现,可以节约内存,降低代码复杂性。

为什么要用HashMap和双链表

上文已经说明HashMap和双链表的各自分工,这里再抛出两个问题:

1)用队列行不行?

不行,队列只能做到先进先出,但是重复用到中间的数据时无法把中间的数据移动到顶端。

2)就用单链表行不行?

也不太行,单链表能实现新来的放头部,最久不用的在尾部删除。但删除的时候需要遍历到尾部,因为单链表只有头指针,在用到已经用到过的数据时,还要遍历整合链表,来确定是否用过,然后再遍历到相应的位置来定位需要跳出的那个节点,并重新放在头部。这效率可想而知。

这时hashmap的作用就出来了,他可以在单位1的时间判断value的值是否存在,key直接存储节点对象,能直接定位到需要跳出的那个节点。

要通过一个节点直接获得父节点的话,单链表(单向性,只能指向下一个)是不行的。这时双向链表的作用也提现出来了,能同时定位到父节点和子节点,使两个节点挂钩,自己快速移到头部挂钩,这效率就很高了。而且由于双向链表有尾指针,所以剔除最后的尾节点也十分方便和快捷。

最后

本文重点针对经典的LUR实现(HashMap和双向链表)进行探秘,帮助自己,时常温故。感兴趣的同学可以看我另外一篇文章《手撸LRU算法基本思路》进一步通过代码视角学习。

我是i猩人,总结不易,转载注明出处,喜欢本篇文章的童鞋欢迎点赞、关注哦。

参考

  • https://zhuanlan.zhihu.com/p/34133067
  • https://my.oschina.net/zjllovecode/blog/1634410
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值