一、缓存置换算法介绍
在面试阿里前端的时候,面试官提出LRU(Least recently used,最近最少使用)的问题,开始的时候犯一些方向性的错误,在面试官的提示下才想出思路。
解决一个问题得分步骤,第一步也是最重要的一步,先搞懂LRU是什么,什么场景下使用LRU?
LRU是什么?
LRU是内存管理的一种页面置换算法,选择最近最久未使用的页面予以淘汰。
通俗一点,内存最多只能存放这么多数据,但是又有新的数据来了,就得从内存里移除一个,给新数据腾出空间。但是删除哪个数据呢?不能随便删一个吧,所以就得制定一个规矩。常见的算法就有LRU、FIFO、LFU。
LRU(Least recently used,最近最少使用)就是把那个最久没有使用过的数据移除。
FIFO(First in first out, 先进先出)就是最先存入的数据最先淘汰。
LFU(Least Frequently Used,最近最少使用)就是最近使用频率最低的被淘汰。
二、算法思路
算法思路如下:
- 判断缓存是否存在,是则转5,否则转2
- 缓存空间是否已满,是则转3,否则转4
- 释放内存
- 存入缓存,转5
- 调用缓存,结束
上述过程如下图:
三、LRU算法
LRU场景如下
前端缓存假设只能存100个值,当101值存入的时候,就得删除原先的一个值,然后放入一个新的值。LRU的思想就是,要把最近最少使用的那个值移除。
问题来了,如何找到哪个值是最近最少使用的?
误区1: 给每个缓存都添加一个时间戳,操作了这个缓存之后更新时间戳,就能找到最近最少使用的缓存了。
误区2: 使用Map数据结构存放数据,将时间戳作为键,缓存作为值
上面两个误区就是我面试的时候踩的严严实实的两个问题,然后大佬帮我分析了一下。
最近最少使用是不是意味着,被调用或者刚存入的缓存就是最新的,然后仔细分析一下存入缓存这个过程:
- 最开始缓存为空,直接存入第一个缓存
- 存入第二个缓存的时候,奇迹发生了,无论是放在第一个前面还是后面,这两个都是有序的。
- 存入第三个时候只要按照前两个的顺序存放,就会保持一个顺序延续下去。这种数据结构像啥——链表呀。
- 根据第二步操作依次添加第三、第四和第五个缓存值,那缓存就是一个链表了,假设表头到表尾就是最新到最久的顺序
考虑一下读取缓存的情况:
- 按顺序遍历缓存,命中缓存时执行第二步,未命中时返回null
- 移除缓存,然后将缓存添加到链表头部
示意图和js实现代码如下:
function Cache(value, key) {
this.value = value;
this.key = key;
}
function LRU() {
this.localCache = [];
this.maxLength = 5;
}
LRU.prototype.getCache = function(key) {
for (var i = 0;i < this.localCache.length; i++) {
if (this.localCache[i].key === key) {
var temp = this.localCache.splice(i, 1);
this.localCache.unshift(temp[0]);
return temp;
}
}
return null;
}
LRU.prototype.setCache = function(cache) {
if (this.localCache.length === this.maxLength) {
this.localCache.pop();
}
this.localCache.unshift(cache);
}
var c1 = new Cache('1', {p: 111});
var c2 = new Cache('2', {p: 222});
var c3 = new Cache('3', {p: 333});
var c4 = new Cache('4', {p: 444});
var c5 = new Cache('5', {p: 555});
var c6 = new Cache('6', {p: 666});
var lru = new LRU();
lru.setCache(c1);
lru.setCache(c2);
lru.setCache(c3);
lru.setCache(c4);
lru.setCache(c5);
console.log(lru);
lru.getCache('3');
console.log(lru);
lru.setCache(c6);
console.log(lru);
四、总结
这里只是对LRU进行简单的实现,很多地方都可以进行扩展的。比如说,将键值与下标放入map,查询效率直接从O(n)变成O(1)。还可以不使用js自带的Array对象实现,而是将每个数据节点变成一个带有前指针和后指针的node。
- LRU算法其实挺简单的,不要走入上面提到的两个误区,这个问题很容易就能分析出来。
- FIFO这个就更简单了,淘汰的时候从一个方向淘汰,被调用的也不需要额外处理。
- LFU算法就比上面两个复杂一点,需要多保存一个数据(缓存使用的次数)。
下篇博客,将会补上另外两种的算法的js实现。