前言
相信有很多同學和我一樣,第一次碰到 LRU(Least Recently Used) 的這個解釋「最近最少使用」都不知道是什麼意思,用湯家鳳老師的話來說:
我真的感到匪夷所思啊!
最近是表示時間,最少是表示頻度,拆開來都知道,但是合在一起就不知道是什麼意思了。經過一番搜尋後,我發現這可能是國內一些專業名詞的通病:翻譯問題。甚至百度百科對 LRU 的解釋也是這樣:
LRU 是 Least Recently Used 的縮寫,即最近最少使用,是一種常用的頁面置換演演算法,選擇最近最久未使用的頁面予以淘汰。該演演算法賦予每個頁面一個訪問欄位,用來記錄一個頁面自上次被訪問以來所經歷的時間 t,當須淘汰一個頁面時,選擇現有頁面中其 t 值最大的,即最近最少使用的頁面予以淘汰。
什麼叫「選擇最近最久未使用的頁面予以淘汰」?今天我們就來摸一摸它的底!
LRU 演演算法過程
LRU 常見的實現是使用一個雙向連結串列儲存快取資料,演演算法步驟如下:
- 插入新資料時會插入到連結串列頭部;
- 當快取資料被訪問時,將該資料移到連結串列尾部;
- 當連結串列滿的時候,將連結串列頭部的資料丟棄。
看完演演算法過程,內心驚呼:這不就是當容量不夠用的時候淘汰最久沒訪問的資料麼,和最少有個毛關係啊!按我的理解,Least Recently 其實應該翻譯成 「最不是最近的」也就是最遠的,Least 其實是修飾 Recently,而不應該和它並列翻譯成「最近最少」,LRU 說到底就是一個時間維度上的快取最佳化演演算法。
LRU 的兄弟 LFU
LFU(Least Frequently Used)和 LRU 比較相似,也是快取最佳化演演算法,但是他和 LRU 唯一的區別就是,LRU 是時間維度上的,當快取滿的時候,將最久沒使用的資料丟棄,而 LFU 是頻率維度上的,會將最少使用(使用頻次最低)的資料丟棄。
百度百科上對 LFU 的解釋如下:
LFU(least frequently used (LFU) page-replacement algorithm)。即最不經常使用頁置換演演算法,要求在頁置換時置換引用計數最小的頁,因為經常使用的頁應該有一個較大的引用次數。但是有些頁在開始時使用次數很多,但以後就不再使用,這類頁將會長時間留在記憶體中,因此可以將引用計數暫存器定時右移一位,形成指數衰減的平均使用次數。
這就有點搞笑了, LFU 的翻譯是「最不經常使用」,既然作為 LRU 的兄弟,難道你不應該翻譯成「最經常最少使用」演演算法嗎?? 不過這好像也側面印證了我上面的猜想應該是正確的。
同樣的翻譯災難
魯棒性
這個名詞乍一聽,這是什麼玩意兒,完全不能從字面意思上推測出實際的含義,結果一查閱發現,這其實是英文 Robustness 的翻譯,應該是直接音譯的,更直觀也更好聽的翻譯應該是「健壯性」、「穩健性」。
有理數
有人提過一個問題:有理數為什麼叫「有理數」?難道是有道理的數?這個答案顯然很離譜,但是我們大多數人也就得過且過吧,沒有去較真了。但是細加挖掘,會發現有理數的英文是 rational number,其中 rational 被翻譯成了「合理的」、「有道理的」,實際上 rational 還有一個意思是「可比的」,也就是能表示成兩個整數之比的數是有理數。
這裡面還有一段很有意思的歷史,感興趣的可以自行查閱。
總結
從這次經歷來看,以後大家要是在一些專業名詞上有困惑,不妨看看它翻譯之前的英文,結合專業名稱具體的含義多多推敲,如果是一個演演算法,就理解它的演演算法過程。最後會發現,一切都有源頭,每個名詞都有它的來歷,不管是翻譯錯誤還是什麼,說不定還能瞭解到背後的一些歷史淵源。
附:LeetCode 146. LRU 快取實現
既然說到了 LRU 的實現,我們不妨將 LeetCode 上關於 LRU 的一道高頻面試題給解決了。
題目要求如下:
請你設計並實現一個滿足 LRU (最近最少使用) 快取 約束的資料結構。
實現
LRUCache
類:
LRUCache(int capacity)
以 正整數 作為容量capacity
初始化 LRU 快取int get(int key)
如果關鍵字key
存在於快取中,則返回關鍵字的值,否則返回-1
。void put(int key, int value)
如果關鍵字key
已經存在,則變更其資料值value
;如果不存在,則向快取中插入該組key-value
。如果插入操作導致關鍵字數量超過capacity
,則應該 逐出 最久未使用的關鍵字。函式
get
和put
必須以O(1)
的平均時間複雜度執行。
首先分析一下這道題需要的資料結構,因為獲取快取元素的 get 方法需要 O(1) 的時間複雜度,因此儲存元素很容易想到使用雜湊表。但是再仔細讀題就會發現,LRU 的核心思想「容量不夠刪除最遠使用的元素」蘊含了資料需要以時間為維度排列成有序序列,且需要頻繁在序列頭部和尾部操作,因此最終選定使用雙向連結串列。
最終,資料結構選取「雜湊表+雙向連結串列」來實現。
首先,先定義雙向連結串列的節點:
var ListNode = function (key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
然後,初始化 LRU 的結構:
var LRUCache = function (capacity) {
this.capacity = capacity;
this.map = new Map(); // 雜湊表
// 初始化雙向連結串列
this.head = new ListNode();
this.tail = new ListNode();
this.head.next = this.tail;
this.tail.prev = this.head;
};
由於演演算法中訪問過的節點要移到連結串列末尾,因此先實現一下節點插入末尾的操作:
LRUCache.prototype.insertTail = function (node) {
this.tail.prev.next = node;
node.prev = this.tail.prev;
node.next = this.tail;
this.tail.prev = node;
}
準備工作做好之後,就開始實現最關鍵的 get 和 put 方法,由於題目中已經將各個情況都描述清楚了,因此按照題目要求進行逐個實現即可。
首先是 get 方法,如果快取中沒有 key,則返回 -1。如果有 key,那麼將該 key 對應的節點從連結串列中原來的位置刪除,然後插入連結串列末尾,代表最近訪問過。最好返回該節點對應的值:
LRUCache.prototype.get = function(key) {
if (!this.map.has(key)) return -1;
let node = this.map.get(key);
// 將 node 從原來位置刪除
node.prev.next = node.next;
node.next.prev = node.prev;
this.insertTail(node); // 插入連結串列末尾
return node.value;
};
然後是 put 方法,如果 key 已經存在,那麼就把對應的節點找出來修改值,然後移到連結串列末尾。如果 key 不存在,要先判斷一下快取是否已滿,如果滿了,要先把頭部節點(最遠使用)刪除,並在雜湊表中刪除記錄,再插入新節點,如果沒滿,就直接插入新節點:
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
// 如果 key 已經存在,則修改值
let node = this.map.get(key);
node.value = value;
// 移到連結串列末尾
node.prev.next = node.next;
node.next.prev = node.prev;
this.insertTail(node);
} else {
// 如果 key 不存在,判斷是否超出容量
if (this.map.size >= this.capacity) {
let rmNode = this.head.next;
// 刪除頭部節點
this.head.next = rmNode.next;
rmNode.next.prev = this.head;
this.map.delete(rmNode.key);
}
// 新建節點插入連結串列末尾,並存入雜湊表
let newNode = new ListNode(key, value);
this.map.set(key, newNode);
this.insertTail(newNode);
}
};
這道中等題在面試中出現的頻率還是比較高的,因此最好還是能掌握。