面試不再怕,20行Python程式碼幫你搞懂LRU演算法

simpleapples發表於2018-03-06

面試不再怕,20行Python程式碼幫你搞懂LRU演算法

LRU演算法在後端工程師面試中,是一個比較常出現的題目,這篇文章帶大家一起,理解LRU演算法,並最終用Python輕鬆實現一個基於LRU演算法的快取。

快取是什麼

面試不再怕,20行Python程式碼幫你搞懂LRU演算法

先看一張圖,當我們訪問網頁,瀏覽器會給伺服器發請求,伺服器會經過一系列的運算,把頁面返回給瀏覽器。

面試不再怕,20行Python程式碼幫你搞懂LRU演算法

當有多個瀏覽器同時訪問的時候,就會在短時間內發起多個請求,而伺服器對每一個請求都要進行一系列相同的操作。重複工作不僅浪費資源,還可能導致響應速度變慢。

面試不再怕,20行Python程式碼幫你搞懂LRU演算法

而快取則可以把伺服器返回的頁面儲存下來,當有其他的瀏覽器再訪問時候,就不必勞伺服器大駕,直接由快取返回頁面。為了保證響應速度,快取通常是基於比較昂貴的硬體,比如RAM,這就決定了我們很難用大量的快取把所有的頁面都存下來,當恰好沒有快取瀏覽器請求的頁面時,依然需要請求伺服器。由於快取容量有限,而資料量無限(網際網路每天新產生的頁面數無法估計),就需要把好剛用在刀刃上,快取那些最有用的資訊。

LRU是什麼

LRU是一種快取淘汰演算法(在OS中也叫記憶體換頁演算法),由於快取空間是有限的,所以要淘汰快取中不常用的資料,留下常用的資料,達到快取效率的最大化。LRU就是這樣一種決定“淘汰誰留下誰”的演算法,LRU是Least recently used的縮寫,從字面意思“最近最少使用”,我們就可以理解LRU的淘汰規則。

LRU的淘汰邏輯

面試不再怕,20行Python程式碼幫你搞懂LRU演算法

我們用一張圖來描述LRU的淘汰邏輯,圖中的快取是一個列表結構,上面是頭結點下面是尾節點,快取容量為8(8個小格子):

  • 有新資料(意味著資料之前沒有被快取過)時,加入到列表頭
  • 快取到達最大容量時,需要淘汰資料多出來的資料,此時淘汰列表尾部的資料
  • 當快取中有資料被命中,則將資料移動到列表頭部(相當於新加入快取)

按上面的邏輯我們可以看到,一個資料如果經常被訪問就會不斷地被移動到列表頭部,不會被淘汰出快取,而越不經常訪問的資料,越容易被擠出快取。

20行Python程式碼實踐LRU

接下來我們用Python來實現一個採用LRU演算法的快取。

從前面的文章中我們可以知道,快取簡化下來就兩個功能,一個是往裡裝資料(快取資料),一個是往外吐資料(命中快取),所以我們的快取對外只需要put和get兩個介面就可以了。

按照前面的示意圖,快取內部我們只需要有一個列表(list)就可以實現LRU邏輯,不過用列表雖然能實現邏輯,但是在判斷是否命中快取時,速度可能非常慢(列表需要遍歷才能知道資料有沒有在裡面)。在Python中,我們可以用基於hash的結構,比如字典(dict)或集合(set),來快速判斷資料是否存在,解決列表實現的效能問題。但是字典和集合又是沒有順序的,如果能有一種既能排序,又是基於hash儲存的資料結構,就好了。

在Python的collections包中,已經內建了這種實用的結構OrderedDict,OrderedDict是dict的子類,但是儲存在內部的元素是有序的(列表的特點)。

解決了資料結構的問題,我們可以直接上手寫邏輯了,程式碼如下:

class LRUCache:

    def __init__(self, capacity):
        self.capacity = capacity
        self.queue = collections.OrderedDict()
    
    def get(self, key):
        if key not in self.queue:
            return -1 // 要找的資料不在快取中返回-1
        value = self.queue.pop(key) // 將命中快取的資料移除
        self.queue[key] = value // 將命中快取的資料重新新增到頭部
        return self.queue[key]
        

    def put(self, key, value):
        if key in self.queue: // 如果已經在快取中,則先移除老的資料
            self.queue.pop(key)
        elif len(self.queue.items()) == self.capacity:
            self.queue.popitem(last=False) // 如果不在快取中並且到達最大容量,則把最後的資料淘汰
        self.queue[key] = value // 將新資料新增到頭部
複製程式碼

下次面試在遇到LRU的題目,是不是就胸有成竹了?

掃碼關注Python私房菜

Python實踐原創內容

相關文章