什麼是 LFU 演算法?
上次的文章介紹了 LRU 演算法,今天打算來介紹一下 LFU 演算法。在上篇文章中有提到, LFU( Least frequently used
:最少使用)演算法與 LRU 演算法只是在淘汰策略上有所不同,LRU 傾向於保留最近有使用的資料,而 LFU 傾向於保留使用頻率較高的資料。
舉一個簡單的:chestnut::快取中有 A、B 兩個資料,且已達到上限,如果 資料 A
先被訪問了 10 次,然後 資料 B
被訪問 1 次,當存入新的 資料 C
時,如果當前是 LRU 演算法,會將 資料 A
淘汰,而如果是 LFU 演算法,則會淘汰 資料 B
。
簡單來說,就是在 LRU 演算法中,不管訪問的頻率,只要最近訪問過,就不會將這個資料淘汰,而在 LFU 演算法中,將訪問的頻率作為權重,只要訪問頻率越高,該資料就越不會被淘汰,即使該資料很久沒有被訪問過。
我們還是通過一段 JavaScript 程式碼來實現這個邏輯。
class LFUCache { freqs = {} // 用於標記訪問頻率 cache = {} // 用於快取所有資料 capacity = 0 // 快取的最大容量 constructor (capacity) { // 儲存 LFU 可快取的最大容量 this.capacity = capacity } }
與 LRU 演算法一樣,LFU 演算法也需要實現 get
與 put
兩個方法,用於獲取快取和設定快取。
class LFUCache { // 獲取快取 get (key) { } // 設定快取 put (key, value) { } }
老規矩,先看設定快取的部分。如果該快取的 key 之前存在,需要更新其值。
class LFUCache { // cache 作為快取的儲存物件 // 其解構為: { key: { freq: 0, value: '' } } // freq 表示該資料讀取的頻率; // value 表示快取的資料; cache = {} // fregs 用於儲存快取資料的頻率 // 其解構為: { 0: [a], 1: [b, c], 2: [d] } // 表示 a 還沒被讀取,b/c 各被讀取1次,d被讀取2次 freqs = {} // 設定快取 put (key, value) { // 先判斷快取是否存在 const cache = this.cache[key] if (cache) { // 如果存在,則重置快取的值 cache.value = value // 更新使用頻率 let { freq } = cache // 從 freqs 中獲取對應 key 的陣列 const keys = this.freqs[freq] const index = keys.indexOf(key) // 從頻率陣列中,刪除對應的 key keys.splice(index, 1) if (keys.length === 0) { // 如果當前頻率已經不存在 key // 將 key 刪除 delete this.freqs[freq] } // 更新頻率加 1 freq = (cache.freq += 1) // 更新頻率陣列 const freqMap = this.freqs[freq] || (this.freqs[freq] = []) freqMap.push(key) return } } }
如果該快取不存在,要先判斷快取是否超過容量,如果超過,需要淘汰掉使用頻率最低的資料。
class LFUCache { // 更新頻率 active (key, cache) { // 更新使用頻率 let { freq } = cache // 從 freqs 中獲取對應 key 的陣列 const keys = this.freqs[freq] const index = keys.indexOf(key) // 從頻率陣列中,刪除對應的 key keys.splice(index, 1) if (keys.length === 0) { // 如果當前頻率已經不存在 key // 將 key 刪除 delete this.freqs[freq] } // 更新頻率加 1 freq = (cache.freq += 1) // 更新讀取頻率陣列 const freqMap = this.freqs[freq] || (this.freqs[freq] = []) freqMap.push(key) } // 設定快取 put (key, value) { // 先判斷快取是否存在 const cache = this.cache[key] if (cache) { // 如果存在,則重置快取的值 cache.value = value this.active(key, cache) return } // 判斷快取是否超過容量 const list = Object.keys(this.cache) if (list.length >= this.capacity) { // 超過儲存大小,刪除訪問頻率最低的資料 const [first] = Object.keys(this.freqs) const keys = this.freqs[first] const latest = keys.shift() delete this.cache[latest] if (keys.length === 0) delete this.freqs[latest] } // 寫入快取,預設頻率為0,表示還未使用過 this.cache[key] = { value, freq: 0 } // 寫入讀取頻率陣列 const freqMap = this.freqs[0] || (this.freqs[0] = []) freqMap.push(key) } }
實現了設定快取的方法後,再實現獲取快取就很容易了。
class LRUCache { // 獲取資料 get (key) { if (this.cache[key] !== undefined) { // 如果 key 對應的快取存在,更新其讀取頻率 // 之前已經實現過,可以直接複用 this.active(key) return this.cache[key] } return undefined } }
關於 LFU 快取演算法實現就到這裡了,當然該演算法一般使用雙鏈表的形式來實現,這裡的實現方式,只是為了方便理解其原理,感興趣的話可以在網上搜索下更加高效的實現方式。
「其他文章」
- 詳解 Webpack devtools
- 什麼是 LFU 演算法?
- 什麼是 LFU 演算法?
- 關於 Promise 的執行順序
- 關於 Promise 的執行順序
- 新一代的編譯工具 SWC
- 介紹一個請求庫 — Undici
- 你給開源專案提過 PR 嗎?
- Go 語言的模組化
- MobX 上手指南
- 介紹兩種 CSS 方法論
- 普通打工人的2020丨掘金年度徵文
- Node.js 服務效能翻倍的祕密(二)
- Node.js 服務效能翻倍的祕密(一)
- 我是如何閱讀原始碼的
- Vue3 Teleport 元件的實踐及原理
- Vue3 模板編譯優化
- 小程式依賴分析實踐
- React 架構的演變 - Hooks 的實現
- Vue 3 的組合 API 如何請求資料?