15.3 複合資料結構
這一類題目通常採用雜湊表或有序表輔助記錄,以加速尋址;再搭配陣列或鏈結串列進行連續的資料儲存,以加速連續選址或刪除值。
146. LRU Cache
題目描述
設計一個固定大小的最近最少使用快取(Least Recently Used Cache, LRU)。當快取未滿時插入資料,或者查找或更新快取內已存在的資料時,將該資料標記為最近使用。在快取滿的情況下,插入新資料時需要移除最久未使用的資料,插入新資料,並將該新資料標記為最近使用。
輸入輸出範例
以下是資料結構的調用範例。給定一個大小為 n 的快取,利用最近最少使用策略儲存資料。
LRUCache cache = new LRUCache( 2 /* capacity */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 輸出 value 1
cache.put(3, 3); // 移除 key 2
cache.get(2); // 輸出 value -1 (未找到)
cache.put(4, 4); // 移除 key 1
cache.get(1); // 輸出 value -1 (未找到)
cache.get(3); // 輸出 value 3
cache.get(4); // 輸出 value 4
題解
我們採用一個鏈結串列來儲存資料的 key 和 value,鏈結串列的連結順序即為最近使用的新舊順序,最新的資料在鏈結串列的頭節點。同時我們需要一個雜湊表進行查找,其鍵是資料的 key,值是其在鏈結串列中的對應指標/迭代 器。每次查找成功(cache hit)時,需要將指標/迭代器對應的節點移動到鏈結串列的頭節點。
- C++
- Python
class LRUCache {
public:
LRUCache(int capacity) : n_(capacity) {}
int get(int key) {
auto it = key_to_cache_it_.find(key);
if (it == key_to_cache_it_.end()) {
return -1;
}
cache_.splice(cache_.begin(), cache_, it->second);
return it->second->second;
}
void put(int key, int value) {
auto it = key_to_cache_it_.find(key);
if (it != key_to_cache_it_.end()) {
it->second->second = value;
return cache_.splice(cache_.begin(), cache_, it->second);
}
cache_.insert(cache_.begin(), make_pair(key, value));
key_to_cache_it_[key] = cache_.begin();
if (cache_.size() > n_) {
key_to_cache_it_.erase(cache_.back().first);
cache_.pop_back();
}
}
private:
list<pair<int, int>> cache_;
unordered_map<int, list<pair<int, int>>::iterator> key_to_cache_it_;
int n_;
};
class Node:
def __init__(self, key=-1, val=-1):
self.key = key
self.val = val
self.prev = None
self.next = None
class LinkedList:
def __init__(self):
self.dummy_start = Node()
self.dummy_end = Node()
self.dummy_start.next = self.dummy_end
self.dummy_end.prev = self.dummy_start
def appendleft(self, node) -> Node:
left, right = self.dummy_start, self.dummy_start.next
node.next = right
right.prev = node
left.next = node
node.prev = left
return node
def remove(self, node) -> Node:
left, right = node.prev, node.next
left.next = right
right.prev = left
return node
def move_to_start(self, node):
return self.appendleft(self.remove(node))
def pop(self):
return self.remove(self.dummy_end.prev)
def peek(self):
return self.dummy_end.prev.val
class LRUCache:
def __init__(self, capacity: int):
self.n = capacity
self.key_to_node = dict()
self.cache_nodes = LinkedList()
def get(self, key: int) -> int:
if key not in self.key_to_node:
return -1
node = self.key_to_node[key]
self.cache_nodes.move_to_start(node)
return node.val
def put(self, key: int, value: int) -> None:
if key in self.key_to_node:
node = self.cache_nodes.remove(self.key_to_node[key])
node.val = value
else:
node = Node(key, value)
self.key_to_node[key] = node
self.cache_nodes.appendleft(node)
if len(self.key_to_node) > self.n:
self.key_to_node.pop(self.cache_nodes.pop().key)
對於 Python 而言,我們還可以直接利用 OrderedDict
函數實現 LRU,這將大大簡化題目的難度。不過,筆者希望讀者還是能仔細研讀以上的解題說明,深入了解 LRU 實現的核心原理。
class LRUCache:
def __init__(self, capacity: int):
self.n = capacity
self.cache = {}
def get(self, key: int) -> int:
if key not in self.cache:
return -1
self.cache[key] = self.cache.pop(key)
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.pop(key)
self.cache[key] = value
if len(self.cache) > self.n:
self.cache.pop(next(iter(self.cache)))
複雜度分析
-
時間複雜度:
get
操作: 平均 ,但因為使用的是內建dict
搭配pop()
和iter()
,在某些 Python 實作中可能會有最壞 。put
操作: 平均 ,但pop(next(iter(...)))
同樣可能在最壞情況下達到 。
-
空間複雜度: ,其中 是快取容量
capacity
,用來儲存最多 個鍵值對。