LRU 居然翻譯成最近最少使用?真相原來是這樣!(附力扣題解)

touryung發表於2023-02-27

前言

相信有很多同學和我一樣,第一次碰到 LRU(Least Recently Used) 的這個解釋「最近最少使用」都不知道是什麼意思,用湯家鳳老師的話來說:

我真的感到匪夷所思啊!

最近是表示時間,最少是表示頻度,拆開來都知道,但是合在一起就不知道是什麼意思了。經過一番搜尋後,我發現這可能是國內一些專業名詞的通病:翻譯問題。甚至百度百科對 LRU 的解釋也是這樣:

LRU 是 Least Recently Used 的縮寫,即最近最少使用,是一種常用的頁面置換演演算法,選擇最近最久未使用的頁面予以淘汰。該演演算法賦予每個頁面一個訪問欄位,用來記錄一個頁面自上次被訪問以來所經歷的時間 t,當須淘汰一個頁面時,選擇現有頁面中其 t 值最大的,即最近最少使用的頁面予以淘汰。

什麼叫「選擇最近最久未使用的頁面予以淘汰」?今天我們就來摸一摸它的底!

LRU 演演算法過程

LRU 常見的實現是使用一個雙向連結串列儲存快取資料,演演算法步驟如下:

  1. 插入新資料時會插入到連結串列頭部;
  2. 當快取資料被訪問時,將該資料移到連結串列尾部;
  3. 當連結串列滿的時候,將連結串列頭部的資料丟棄。

看完演演算法過程,內心驚呼:這不就是當容量不夠用的時候淘汰最久沒訪問的資料麼,和最少有個毛關係啊!按我的理解,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);
  }
};

這道中等題在面試中出現的頻率還是比較高的,因此最好還是能掌握。

相關文章