一、緩存置換算法介紹
在面試阿里前端的時候,面試官提出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實現。