這篇文章描述了怎麼用 Python 實現複雜度為 O(1) 的「最不常用」(Least Frequently Used, LFU)快取回收演算法。在 Ketan Shah、Anirban Mitra 和 Dhruv Matani的論文中有演算法描述。實現中的命名是按照論文中的命名。
LFU 快取回收機制對於 HTTP 快取網路代理是非常有用的,我們可以從快取中移除那些最不常使用的條目。
本文旨在設計一個其所有操作的時間複雜度都只有 O(1)的 LFU 快取演算法,這些操作包括了插入、訪問和刪除(回收)。
這個演算法中用了雙向連結串列。其一是用於訪問頻率,連結串列中的每個結點都包含一個連結串列,其中的元素有相同的訪問頻率。假設快取中有5個元素。有兩個元素被訪問了一次,三個元素被訪問了兩次。在這個例子中,訪問頻率列表有兩個結點(頻率為1和2)。第一個頻率結點的連結串列中有兩個結點,第二個頻率結點的連結串列中有三個結點。
我們要怎麼構建它呢?我們需要的第一個物件是結點:
1 2 3 4 5 6 |
class Node(object): """Node containing data, pointers to previous and next node.""" def __init__(self, data): self.data = data self.prev = None self.next = None |
接下來是雙向連結串列。每個結點有 prev 和 next 屬性,分別等於前一個和下一個結點。head 被設為第一個結點,tail 被設為最後一個結點。
我們可以為雙向連結串列定義方法來在連結串列尾部加入結點,插入結點,刪除結點以及獲得連結串列所有結點的資料。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
class DoublyLinkedList(object): def __init__(self): self.head = None self.tail = None # Number of nodes in list. self.count = 0 def add_node(self, cls, data): """Add node instance of class cls.""" return self.insert_node(cls, data, self.tail, None) def insert_node(self, cls, data, prev, next): """Insert node instance of class cls.""" node = cls(data) node.prev = prev node.next = next if prev: prev.next = node if next: next.prev = node if not self.head or next is self.head: self.head = node if not self.tail or prev is self.tail: self.tail = node self.count += 1 return node def remove_node(self, node): if node is self.tail: self.tail = node.prev else: node.next.prev = node.prev if node is self.head: self.head = node.next else: node.prev.next = node.next self.count -= 1 def get_nodes_data(self): """Return list nodes data as a list.""" data = [] node = self.head while node: data.append(node.data) node = node.next return data |
訪問頻率雙向連結串列中的每個結點都是一個頻率結點(下圖中的Freq Node)。它是一個結點,同時也是一個包含有相同頻率的元素(下圖中Item node)的雙向性連結串列。每個條目結點都有一個指向其頻率結點父親的指標。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class FreqNode(DoublyLinkedList, Node): """Frequency node containing linked list of item nodes with same frequency.""" def __init__(self, data): DoublyLinkedList.__init__(self) Node.__init__(self, data) def add_item_node(self, data): node = self.add_node(ItemNode, data) node.parent = self return node def insert_item_node(self, data, prev, next): node = self.insert_node(ItemNode, data, prev, next) node.parent = self return node def remove_item_node(self, node): self.remove_node(node) class ItemNode(Node): def __init__(self, data): Node.__init__(self, data) self.parent = None |
條目結點的資料等於我們要儲存的元素的鍵,這個鍵可以是一條HTTP請求。內容本身(例如HTTP響應)儲存在字典中。字典中的每個值是LfuItem型別,”data”是快取的內容,”parent”是指向頻率結點的指標,”node”是指向頻率結點下條目結點的指標。
1 2 3 4 5 |
class LfuItem(object): def __init__(self, data, parent, node): self.data = data self.parent = parent self.node = node |
我們已經定義了資料物件類,現在可以定義快取物件類了。它有一個雙向連結串列(訪問頻率連結串列)和一個包含LFU條目(上面的LfuItem)的字典。我們定義兩個方法:一個用來插入頻率結點,一個用來刪除頻率結點。
1 2 3 4 5 6 7 8 9 10 |
class Cache(DoublyLinkedList): def __init__(self): DoublyLinkedList.__init__(self) self.items = dict() def insert_freq_node(self, data, prev, next): return self.insert_node(FreqNode, data, prev, next) def remove_freq_node(self, node): self.remove_node(node) |
下一步是定義方法來插入到快取,訪問快取以及從快取中刪除。
我們來看看插入方法的邏輯。它以一個鍵和值為引數,例如HTTP請求和響應。如果沒有頻率為1的頻率結點,它就被插入到訪問頻率雙向連結串列的開頭。一個條目結點被加入到頻率結點的條目雙向連結串列。鍵和值被加入到字典中。複雜度是O(1)。
1 2 3 4 5 6 7 8 9 |
def insert(self, key, value): if key in self.items: raise DuplicateException('Key exists') freq_node = self.head if not freq_node or freq_node.data != 1: freq_node = self.insert_freq_node(1, None, freq_node) freq_node.add_item_node(key) self.items[key] = LfuItem(value, freq_node) |
我們在快取中插入兩個元素,得到:
我們來看看訪問方法的邏輯。如果鍵不存在,我們丟擲異常。如果鍵存在,我們把條目結點移到頻率加一的頻率結點的連結串列中(如果頻率結點不存在就增加這個結點)。複雜度是O(1)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def access(self, key): try: tmp = self.items[key] except KeyError: raise NotFoundException('Key not found') freq_node = tmp.parent next_freq_node = freq_node.next if not next_freq_node or next_freq_node.data != freq_node.data + 1: next_freq_node = self.insert_freq_node(freq_node.data + 1, freq_node, next_freq_node) item_node = next_freq_node.add_item_node(key) tmp.parent = next_freq_node freq_node.remove_item_node(tmp.node) if freq_node.count == 0: self.remove_freq_node(freq_node) tmp.node = item_node return tmp.data |
如果我們訪問Key 1的條目,這個條目結點就被移動到頻率為2的頻率結點之下。我們得到:
如果我們訪問Key 2的條目,這個條目結點就被移動到頻率為2的頻率結點之下。頻率為1的頻率結點會被刪除(譯註:因為它之下沒有條目結點了),我們得到:
我們再看看delete_lfu方法。它把最不常使用的條目從快取中刪除。為此,它刪除第一個頻率結點下的第一個條目結點,同時從字典刪除對應的LFUItem物件。如果此操作過後,頻率結點的連結串列為空,就刪除這個頻率結點。
1 2 3 4 5 6 7 8 9 10 11 12 |
def delete_lfu(self): """Remove the first item node from the first frequency node. Remove the item from the dictionary. """ if not self.head: raise NotFoundException('No frequency nodes found') freq_node = self.head item_node = freq_node.head del self.items[item_node.data] freq_node.remove_item_node(item_node) if freq_node.count == 0: self.remove_freq_node(freq_node) |
如果在快取上呼叫delete_lfu,資料為Key 1的條目結點和它的LFUItem將被刪除。我們得到:
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式