JavaScript實現(xiàn)LRU緩存的三種方式詳解
LRU全稱為Least Recently Used,即最近使用的。針對的是在有限的內(nèi)存空間內(nèi),只緩存最近使用的數(shù)據(jù)(即get和set的數(shù)據(jù)),超過有限內(nèi)存空間的數(shù)據(jù)將會被刪除。這個在面試題中也是常會被問到的內(nèi)容,接下來就看看怎么來實現(xiàn)。
分析
從定義來看,LRU至少有兩個特性:通過鍵值對讀寫、有序。實現(xiàn)鍵值對讀寫,一般我們會使用哈希表來表示,注意哈希表是一個邏輯結(jié)構(gòu),實際上我們需要使用object
、map
等來實現(xiàn);數(shù)據(jù)有序,即最近使用的數(shù)據(jù)放在前面,已經(jīng)過時的數(shù)據(jù)放在后面或被刪除,并且支持?jǐn)?shù)據(jù)是可排序的,我們可以想到數(shù)組、鏈表、map之類的數(shù)據(jù)格式。因此,我們有三種方式可以實現(xiàn)LRU緩存:
- Map
- Object + Array
- LinkedList
使用Map實現(xiàn)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},不會按照原始的插入順序
那么我們就可以根據(jù)Map
的特性實現(xiàn)LRU,下面是原理圖:
實現(xiàn)代碼:
class LRUCache { data = new Map(); // 數(shù)據(jù)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); } // 將數(shù)據(jù)對象添加到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; // 數(shù)據(jù)map中沒有key對應(yīng)的數(shù)據(jù),則返回null if (!data.has(key)) return null; const value = data.get(key); // 返回數(shù)據(jù)前,先刪除原數(shù)據(jù),然后在添加,就可以保持在最新 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}
運(yùn)行結(jié)果:
使用Map
基本上就可以實現(xiàn)LRU緩存,并且其兼容性還可以,除了IE兼容性差點:
如果不使用Map,那么還可以使用什么方式實現(xiàn)LRU緩存呢?
使用Object + Array實現(xiàn)LRU緩存
剛剛我們也分析了,LRU需要滿足兩點:鍵值對和可排序,這兩點可以分別對應(yīng)到Object
和Array
,那么我們是不是就可以以此為思路實現(xiàn)呢?答案是可以的,實現(xiàn)代碼:
class LRUCacheObjArr { length = 0; // 定義列表最大長度 dataObj = {}; // 使用對象暫存數(shù)據(jù),在可保證get時間復(fù)雜度為O(1) dataArr = []; // 使用數(shù)組解決有序的問題 constructor(length) { if (length < 1) throw new Error('參數(shù)非法') this.length = length; } get(key) { if (!this.dataObj[key] || !this.dataArr.length) return null; // 需要將訪問到的值,重新放在數(shù)組的最末尾,表示最新的數(shù)據(jù) const index = this.dataArr.findIndex(item => item.key === key); // 先刪除原數(shù)據(jù),然后push到數(shù)組末尾 this.dataArr.splice(index, 1); this.dataArr.push(this.dataObj[key]); // 返回值,對象是無序的,我們只需要保證里面的數(shù)據(jù)是最新的即可 return this.dataObj[key].value; } set(key, value) { // 定義對象數(shù)據(jù) const obj = {key, value}; const index = this.dataArr.findIndex(item => item.key === key); // 判斷是否為新增還是修改 if (index !== -1) { // 如果已存在數(shù)據(jù),則先刪除,然后push到末尾 this.dataArr.splice(index, 1); this.dataArr.push(obj); } else { // 如果不存在數(shù)據(jù),則數(shù)組直接push this.dataArr.push(obj); } // 對象新增或者修改同一個對象 this.dataObj[key] = obj; // 判斷新增數(shù)據(jù)后,是否超過最大長度 if (this.dataArr.length > this.length) { // 數(shù)組是有序的,超長后直接從頭部刪除,并且獲取刪除的數(shù)據(jù) const tmp = this.dataArr.shift(); // 數(shù)據(jù)對象里面也應(yīng)該刪除當(dāng)前刪除的對象,避免被再次取到 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}
運(yùn)行結(jié)果:
使用對象+數(shù)組的方式,雖然可以實現(xiàn)效果,但是由于會頻繁操作數(shù)組,因此會犧牲一些性能,而且實現(xiàn)起來也沒有Map
方便。除了使用數(shù)組+對象,其實我們還可以使用雙向鏈表的方式實現(xiàn)LRU。
使用雙向鏈表實現(xiàn)LRU
雙向鏈表,顧名思義就是兩個方向的鏈表,這有別于單向鏈表。直接看圖可能更直觀一點:
雙向鏈表在移動元素時,會比單向鏈表復(fù)雜一點,,例如我們想把B和C節(jié)點交換一下位置,其過程如下:
這對應(yīng)到LRU有什么意義呢?雙向鏈表是有序的,每一個節(jié)點都知道其上一個或者下一個節(jié)點;其存值的方式也是使用鍵值對的方式,因此完全可以實現(xiàn)LRU。
實現(xiàn)代碼:
class LRUCacheLinked { data = {}; // 鏈表數(shù)據(jù) dataLength = 0; // 鏈表長度,使用變量保存,可以更快訪問 listHead = null; // 鏈表頭部 listTail = null; // 鏈表尾部 length = 0; // 鏈表最大長度 // 構(gòu)造函數(shù) constructor(length) { if (length < 1) throw new Error('參數(shù)不合法') this.length = length; } set(key, value) { const curNode = this.data[key]; if (curNode == null) { // 新增數(shù)據(jù) const nodeNew = {key, value}; // 移動到末尾 this.moveToTail(nodeNew); // 將新增的節(jié)點保存到數(shù)據(jù)對象中,其pre或next將在moveToTail中設(shè)置 this.data[key] = nodeNew; // 鏈表長度遞增 this.dataLength++; // 初始化鏈表頭部 if (this.dataLength === 1) this.listHead = nodeNew; } else { // 修改現(xiàn)有數(shù)據(jù) curNode.value = value; // 移動到末尾 this.moveToTail(curNode); } // 添加完數(shù)據(jù)后可能會導(dǎo)致超出長度,因此嘗試著刪除數(shù)據(jù) this.tryClean(); } get(key) { // 根據(jù)key值取出對應(yīng)的節(jié)點 const curNode = this.data[key]; if (curNode == null) return null; if (this.listTail === curNode) { // 如果被訪問的元素處于鏈表末尾,則直接返回值,且不用修改鏈表 return curNode.value; } // 如果是中間元素或者頭部元素,則在獲取前需要將其移動到鏈表尾部,表示最新 this.moveToTail(curNode); return curNode.value; } // 移動節(jié)點到鏈表尾部 moveToTail(curNode) { // 獲取鏈表尾部 const tail = this.listTail; // 如果當(dāng)前節(jié)點就是鏈表尾部,則不做處理,直接返回 if (tail === curNode) return; // 1. preNode和nextNode斷絕與curNode之間的關(guān)系 const preNode = curNode.pre; const nextNode = curNode.next; if (preNode) { if (nextNode) { // 當(dāng)前元素的上一個節(jié)點指向其下一個 preNode.next = nextNode; } else { // 斷開當(dāng)前元素與上一個節(jié)點的聯(lián)系 delete preNode.next; } } if (nextNode) { if (preNode) { // 當(dāng)前元素的下一個節(jié)點指向其上一個 nextNode.pre = preNode; } else { // 斷開當(dāng)前元素與下一個節(jié)點的關(guān)系 delete nextNode.pre; } // 如果當(dāng)前節(jié)點是鏈表頭部,則需要重新賦值 if (this.listHead === curNode) this.listHead = nextNode; } // 2. curNode斷絕與preNode和nextNode之間的關(guān)系 delete curNode.pre delete curNode.next // 3. 在list末尾,重新建立curNode的新關(guān)系 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('鏈表頭部缺失下一個節(jié)點'); // 1. 斷絕head和headNext之間的關(guān)系 delete head.next; delete headNext.pre; // 2. 重新賦值listHead this.listHead = headNext; // 3. 清理data delete this.data[head.key]; // 4. 重新計數(shù) this.dataLength = this.dataLength - 1; } } }
這么一看,雙向鏈表是這三種方式中最復(fù)雜的一個。我們使用同樣的案例測試一下:
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'));
實現(xiàn)結(jié)果:
總結(jié)
本文總結(jié)了三種實現(xiàn)LRU緩存的方法,其中使用Map
是最佳的方式。使用其他兩種方式,雖然可以實現(xiàn)效果,但是從效率、可讀性上來看,還是Map
更勝一籌。這三種方式你都學(xué)會了嗎?
到此這篇關(guān)于JavaScript實現(xiàn)LRU緩存的三種方式詳解的文章就介紹到這了,更多相關(guān)JavaScript LRU緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
封裝了一個支持匿名函數(shù)的Javascript事件監(jiān)聽器
這篇文章主要介紹了支持匿名函數(shù)的Javascript事件監(jiān)聽封裝,需要的朋友可以參考下2014-06-06兩款JS腳本判斷手機(jī)瀏覽器類型跳轉(zhuǎn)WAP手機(jī)網(wǎng)站
本文通過兩款js腳本判斷手機(jī)瀏覽器類型跳轉(zhuǎn)到wap手機(jī)網(wǎng)站,感興趣的小伙伴快來學(xué)習(xí)吧2015-10-10js實現(xiàn)黑色簡易的滑動門網(wǎng)頁tab選項卡效果
這篇文章主要介紹了js實現(xiàn)黑色簡易的滑動門網(wǎng)頁tab選項卡效果,可實現(xiàn)簡單的鼠標(biāo)滑過tab項切換對應(yīng)菜單的功能,涉及javascript鼠標(biāo)事件控制頁面元素的遍歷與樣式改變實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-08-08手把手教你實現(xiàn)一個JavaScript時間軸組件
本文主要是給大家?guī)硪粋€時間軸的組件開發(fā)教程,其主要功能就是可以拖動時間軸來定位當(dāng)前時間,可以通過鼠標(biāo)滾輪來修改當(dāng)前時間分辨率,需要的可以參考一下2022-10-10uniapp實現(xiàn)app檢查更新與升級詳解(uni-upgrade-center)
UniApp是一個跨平臺的開發(fā)框架,可以同時開發(fā)iOS和Android應(yīng)用,下面這篇文章主要給大家介紹了關(guān)于uniapp實現(xiàn)app檢查更新與升級(uni-upgrade-center)的相關(guān)資料,需要的朋友可以參考下2023-11-11JavaScript屏蔽指定區(qū)域內(nèi)右鍵菜單
有時候需要屏蔽部分區(qū)域內(nèi)的右鍵菜單,下面的代碼大家可以測試下。2010-03-03一個html5播放視頻的video控件只支持android的默認(rèn)格式mp4和3gp
寫了個html5播放視頻的video控件,只支持mp4和3gp(android和ios默認(rèn)支持的格式就寫了這個) ,需要的朋友可以參考下2014-05-05