題目
運用你所掌握的數據結構,設計和實現一個 LRU (最近最少使用) 緩存機制
。它應該支持以下操作: 獲取數據 get
和 寫入數據 put
。
獲取數據 get(key)
- 如果關鍵字 (key) 存在於緩存中,則獲取關鍵字的值(總是正數),否則返回 -1。
寫入數據 put(key, value)
- 如果關鍵字已經存在,則變更其數據值;如果關鍵字不存在,則插入該組「關鍵字/值」。當緩存容量達到上限時,它應該在寫入新數據之前刪除最久未使用的數據值,從而爲新的數據值留出空間。
進階:
你是否可以在 O(1) 時間複雜度內完成這兩種操作?
示例:
LRUCache cache = new LRUCache( 2 /* 緩存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 該操作會使得關鍵字 2 作廢
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 該操作會使得關鍵字 1 作廢
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
解題思路
LRU 緩存機制可以通過哈希表輔以雙向鏈表實現,我們用一個哈希表和一個雙向鏈表維護所有在緩存中的鍵值對。
雙向鏈表按照被使用的順序存儲了這些鍵值對,靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的。哈希表即爲普通的哈希映射(HashMap),通過緩存數據的鍵映射到其在雙向鏈表中的位置。
我們首先使用哈希表進行定位,找出緩存項在雙向鏈表中的位置,隨後將其移動到雙向鏈表的頭部,即可在 O(1) 的時間內完成 get 或者 put 操作。具體方法如下:
1)對於 get 操作,首先判斷 key 是否存在:
1.1)如果 key 不存在,則返回 -1;
1.2)如果 key 存在,則 key 對應的節點是最近被使用的節點。通過哈希表定位到該節點在雙向鏈表中的位置,並將其移動到雙向鏈表的頭部,最後返回該節點的值。
2)對於 put 操作,首先判斷 key 是否存在:
2.1)如果 key 存在,則與 get 操作類似,先通過哈希表定位,再將對應的節點的值更新爲 value,並將該節點移到雙向鏈表的頭部。
2.2)如果 key 不存在,先判斷雙向鏈表的節點數是否超出容量,如果超出容量,則刪除雙向鏈表的尾部節點,並刪除哈希表中對應的項。再使用 key 和 value 創建一個新的節點,在雙向鏈表的頭部添加該節點,並將 key 和該節點添加進哈希表中。
上述各項操作中,訪問哈希表的時間複雜度爲 O(1),在雙向鏈表的頭部添加節點、在雙向鏈表的尾部刪除節點的複雜度也爲 O(1)。而將一個節點移到雙向鏈表的頭部,可以分成「刪除該節點」和「在雙向鏈表的頭部添加節點」兩步,都可以在 O(1) 時間內完成。
在雙向鏈表的實現中,使用一個僞頭部(dummy head)和僞尾部(dummy tail)標記界限,這樣在添加節點和刪除節點的時候就不需要檢查相鄰的節點是否存在。
複雜度分析:
時間複雜度:對於 put 和 get 都是 O(1)。
空間複雜度:O(capacity),因爲哈希表和雙向鏈表最多存儲 capacity 個元素。
代碼
# 定義雙向鏈表結構
class Node:
def __init__(self, key, val):
self.key = key
self.val = val
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.dic = {}
self.capacity = capacity
# 初始化頭節點
self.head = Node(None, None)
# 初始化尾節點
self.tail = Node(None, None)
# 將頭節點和尾節點連接起來
self.head.next = self.tail
self.tail.prev = self.head
# 在鏈表頭部添加節點
def insert(self, node):
node.next = self.head.next
node.prev = self.head
temp = self.head.next
self.head.next = node
temp.prev = node
# 刪除節點
def delete(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def get(self, key: int) -> int:
# 如果不在字典中,返回-1
if not key in self.dic:
return -1
node = self.dic[key]
# 先刪除,再插入
self.delete(node)
self.insert(node)
return node.val
def put(self, key: int, value: int) -> None:
# 如果在字典中,直接更改value,再執行刪除,插入兩步驟,相當於把節點移到了雙向鏈表的開頭。
if key in self.dic:
node = self.dic[key]
node.val = value
self.delete(node)
self.insert(node)
# return
# 如果不在字典中
else:
# 先看緩存滿了沒有,如果達到緩存容量限制,刪除最久未使用的數據值,即最後一個節點
if len(self.dic) == self.capacity:
node = self.tail.prev
self.delete(node)
del self.dic[node.key]
# 插入節點
node = Node(key, value)
self.dic[key] = node
self.insert(node)
# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)