LRU Cache
Tạo bộ nhớ đệm chứa tối đa capacity khóa. get key trả về giá trị của khóa, hoặc -1 nếu khóa chưa có trong bộ nhớ đệm. put key value lưu giá trị, thay thế giá trị cũ nếu có; nếu khóa còn mới và bộ nhớ đệm đã đầy, trước tiên hãy loại bỏ khóa ít được dùng gần đây nhất. Một thao tác get tìm thấy khóa và mọi thao tác put đều được tính là một lần sử dụng.
Thao tác i là ops[i] ("get" hoặc "put") trên khóa keys[i], với giá trị values[i] (0 cho thao tác get). Thực hiện các thao tác theo thứ tự trên bộ nhớ đệm trống và trả về một chuỗi cho mỗi thao tác: "null" cho thao tác put, giá trị hoặc "-1" cho thao tác get.
Hàm
- capacityinteger
- số lượng khóa tối đa mà bộ nhớ đệm có thể lưu cùng lúc
- opsstring-array
- các phép toán, theo thứ tự chúng được thực hiện
- keysinteger-array
- khóa mà mỗi thao tác tác động lên
- valuesinteger-array
- giá trị cho mỗi put, 0 cho mỗi get
- Trả vềstring-array
- mỗi phép toán có một đáp án, dưới dạng văn bản
Ràng buộc
1 ≤ capacity ≤ 30001 ≤ ops.length ≤ 3000keys.length == values.length == ops.length- Mỗi
ops[i]làgethoặcput. 0 ≤ keys[i] ≤ 1090 ≤ values[i] ≤ 109với thao tác put, vàvalues[i] == 0với thao tác get.
Ví dụ
- Đầu vào
- capacity = 2ops = ["put", "put", "get", "put", "get", "get"]keys = [1, 2, 1, 3, 2, 3]values = [10, 20, 0, 30, 0, 0]
- Đầu ra
- ["null", "null", "10", "null", "-1", "30"]
- Giải thích
- get khiến khóa 1 trở thành khóa được dùng gần đây nhất, nên
put 3loại bỏ khóa 2:get 2trả về"-1"vàget 3trả về"30".
- Đầu vào
- capacity = 2ops = ["put", "put", "put", "put", "get", "get", "get"]keys = [5, 6, 5, 7, 5, 6, 7]values = [1, 2, 3, 4, 0, 0, 0]
- Đầu ra
- ["null", "null", "null", "null", "3", "-1", "4"]
- Giải thích
- Lần gọi put thứ ba cập nhật khóa 5, khóa này đã có trong bộ nhớ đệm: không có khóa nào bị loại bỏ và khóa 5 trở thành khóa được dùng gần đây nhất. Vì vậy,
put 7loại bỏ khóa 6, không phải khóa 5.
- Đầu vào
- capacity = 1ops = ["get", "put", "get", "put", "get", "get"]keys = [0, 0, 0, 8, 0, 8]values = [0, 0, 0, 8, 0, 0]
- Đầu ra
- ["-1", "null", "0", "null", "-1", "8"]
- Giải thích
- Với sức chứa một khóa, mỗi khóa mới sẽ đẩy khóa trước đó ra. Lần gọi get đầu tiên không tìm thấy gì trong bộ nhớ đệm trống; sau đó, khóa 0 giữ giá trị thực 0 cho đến khi
put 8đẩy nó ra.
Câu hỏi mở rộng
Làm thế nào để bạn thay đổi bộ nhớ đệm sao cho thay vào đó, bộ nhớ đệm loại bỏ khóa ít được sử dụng nhất, nếu hòa thì loại bỏ khóa ít được sử dụng gần đây nhất, mà mỗi thao tác vẫn có độ phức tạp O(1)?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Để loại bỏ đúng khóa, bạn cần biết thứ tự sử dụng gần đây nhất của các khóa. Cấu trúc nào duy trì thứ tự và cho phép bạn chuyển bất kỳ phần tử nào lên đầu hoặc lấy một phần tử ở cuối mà không làm dịch chuyển các phần tử khác?
Danh sách liên kết đôi xóa một nút khỏi danh sách hoặc chèn nút vào đầu danh sách bằng cách thay đổi bốn liên kết, nhưng chỉ khi bạn đã có nút đó. Bản đồ băm ánh xạ mỗi khóa đến nút tương ứng giúp bạn lấy nút đó trong O(1).
Giữ nút được dùng gần đây nhất ở đầu và nút ít được dùng gần đây nhất ở cuối. Khi get hit hoặc có bất kỳ thao tác put nào, hãy chuyển nút lên đầu. Khi có khóa mới và bộ nhớ đệm đã đầy, hãy xóa nút cuối danh sách và khóa tương ứng khỏi bản đồ. Hai nút canh ở hai đầu giúp bạn không phải kiểm tra danh sách có rỗng hay không.
Lời giải
Thao tác get phải tìm khóa nhanh, còn thao tác loại bỏ phải nhanh chóng tìm được khóa ít được dùng gần đây nhất. Bảng băm làm việc thứ nhất, còn danh sách được sắp xếp theo thứ tự sử dụng làm việc thứ hai. Hãy kết hợp cả hai: ánh xạ mỗi khóa đến nút của nó trong danh sách liên kết kép được sắp xếp theo thứ tự sử dụng.
Quét bảng các mục cùng thời điểm sử dụng gần nhất
Ý tưởng
Lưu tối đa capacity mục trong các mảng: khóa, giá trị và dấu thời gian sử dụng gần nhất lấy từ bộ đếm tăng sau mỗi thao tác. Khi bộ nhớ đệm đã đầy mà có khóa mới, mục có dấu thời gian nhỏ nhất sẽ bị ghi đè, tức là khóa lâu nhất chưa được dùng. Trong ví dụ đầu tiên, ở thao tác put 3, khóa 1 có dấu thời gian 3 và khóa 2 có dấu thời gian 2, nên khóa 2 sẽ bị loại bỏ.
Mỗi thao tác quét tối đa capacity mục: O(n × capacity) cho n thao tác, tức là hàng triệu phép so sánh khi thực hiện 3.000 thao tác trên bộ nhớ đệm có 1.500 khóa.
Thuật toán
- Duy trì các mảng
keys,valuesvàused, một biến đếm số phần tử đã điền và một đồng hồ bắt đầu ở 0. - Mỗi thao tác tăng đồng hồ thêm 1 rồi tìm khóa của nó trong các phần tử đã điền.
get: nếu không tìm thấy, trả về-1; nếu tìm thấy, đặtusedbằng giá trị đồng hồ rồi trả về giá trị.put: nếu tìm thấy, lưu giá trị và giá trị đồng hồ. Nếu không tìm thấy, ghi khóa, giá trị và giá trị đồng hồ vào một phần tử còn trống hoặc phần tử cóusednhỏ nhất.
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
# Entry i holds keys[i] -> values[i] and was last used at time used[i].
self.keys = [0] * capacity
self.values = [0] * capacity
self.used = [0] * capacity
self.size = 0
self.time = 0
def _find(self, key):
for i in range(self.size):
if self.keys[i] == key:
return i
return -1
def get(self, key):
self.time += 1
i = self._find(key)
if i == -1:
return -1
self.used[i] = self.time
return self.values[i]
def put(self, key, value):
self.time += 1
i = self._find(key)
if i == -1:
if self.size < self.capacity:
i = self.size
self.size += 1
else:
# Full: scan for the entry used longest ago and take its place.
i = 0
for j in range(1, self.capacity):
if self.used[j] < self.used[i]:
i = j
self.keys[i] = key
self.values[i] = value
self.used[i] = self.time
def lruCacheOps(capacity, ops, keys, values):
cache = LRUCache(capacity)
result = []
for op, key, value in zip(ops, keys, values):
if op == "put":
cache.put(key, value)
result.append("null")
else:
result.append(str(cache.get(key)))
return resultBản đồ băm kết hợp với danh sách liên kết kép
Ý tưởng
Mỗi nút trong danh sách liên kết với cả hai nút bên cạnh, nên khi đã có nút đó, bạn có thể tách nó ra hoặc chèn nó vào đầu danh sách bằng cách thay đổi bốn liên kết. Bản đồ cung cấp nút đó cho bạn, nên không thao tác nào phải duyệt danh sách.
Khi tìm thấy khóa hoặc thêm một khóa đã được lưu vào bộ nhớ đệm, nút đó sẽ được chuyển lên đầu danh sách. Khi bộ nhớ đệm đầy và có khóa mới, nút cuối danh sách sẽ bị xóa và khóa của nó bị xóa khỏi bản đồ, vì vậy mỗi nút lưu khóa riêng. Các nút gác head và tail luôn nằm ở hai đầu, nên thao tác tách và chèn không bao giờ cần kiểm tra xem có nút bên cạnh nào bị thiếu không. Mỗi thao tác gồm một lần tra cứu và một vài thay đổi liên kết: O(1).
Thuật toán
- Nối các nút lính canh
headvàtail; bắt đầu với một ánh xạ rỗng từ khóa đến nút. get key: trả về-1nếu khóa không có trong ánh xạ; nếu có, chuyển nút của khóa đến ngay sauheadrồi trả về giá trị của nút.putvới khóa đã lưu: lưu giá trị và chuyển nút đến ngay sauhead.putvới khóa mới: nếu ánh xạ đang chứacapacitykhóa, gỡ nút ngay trướctailvà xóa khóa của nút đó khỏi ánh xạ. Chèn nút mới ngay sauheadvà ánh xạ khóa đến nút đó.
class ListNode:
__slots__ = ("key", "value", "prev", "next")
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.nodes = {} # key -> its node in the list
# Sentinels: head.next is the most recently used node,
# tail.prev the least recently used one.
self.head = ListNode()
self.tail = ListNode()
self.head.next = self.tail
self.tail.prev = self.head
def _unlink(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _push_front(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key):
node = self.nodes.get(key)
if node is None:
return -1
self._unlink(node)
self._push_front(node)
return node.value
def put(self, key, value):
node = self.nodes.get(key)
if node is not None:
node.value = value
self._unlink(node)
else:
if len(self.nodes) == self.capacity:
# Full: drop the least recently used node.
oldest = self.tail.prev
self._unlink(oldest)
del self.nodes[oldest.key]
node = ListNode(key, value)
self.nodes[key] = node
self._push_front(node)
def lruCacheOps(capacity, ops, keys, values):
cache = LRUCache(capacity)
result = []
for op, key, value in zip(ops, keys, values):
if op == "put":
cache.put(key, value)
result.append("null")
else:
result.append(str(cache.get(key)))
return result
Lỗi thường gặp và trường hợp biên
- Không đưa khóa lên đầu khi truy cập thành công bằng get. Trong ví dụ đầu tiên,
put 3sẽ loại bỏ khóa 1 chứ không phải khóa 2. - Loại bỏ phần tử trước khi kiểm tra xem khóa có được lưu trong bộ nhớ đệm hay không. Với sức chứa là 2,
put 1,put 2,put 2sẽ loại bỏ khóa 1. - Gỡ một nút khỏi danh sách liên kết nhưng vẫn giữ khóa của nó trong từ điển, hoặc ngược lại. Khi đó, từ điển trả về một nút đã bị tách khỏi danh sách, và việc di chuyển nút này làm hỏng các liên kết của những nút liền kề trước đó.
- Coi giá trị 0 đã lưu là một lần không tìm thấy. Trong ví dụ thứ ba,
get 0phải trả về"0".
Câu hỏi thường gặp3
Độ phức tạp thời gian của bộ nhớ đệm LRU là bao nhiêu?
Với một bảng băm và một danh sách liên kết kép, cả get và put đều chạy trong O(1): một lần tra cứu cùng một vài thay đổi liên kết. Độ phức tạp không gian là O(capacity), với một nút và một mục trong bảng cho mỗi khóa.
Tại sao bộ nhớ đệm LRU sử dụng danh sách liên kết đôi?
Bộ nhớ đệm đưa các khóa từ bất kỳ vị trí nào trong thứ tự lên đầu và loại bỏ khóa ở cuối. Danh sách liên kết đôi làm được cả hai việc chỉ với vài thay đổi liên kết, vì mỗi nút đều biết nút đứng trước nó. Danh sách liên kết đơn cần duyệt từ đầu danh sách để tìm nút đó, với độ phức tạp O(capacity).
Bạn có thể tạo bộ nhớ đệm LRU bằng từ điển có thứ tự không?
Đúng. Một map giữ thứ tự chèn, chẳng hạn như OrderedDict của Python hoặc LinkedHashMap của Java, có thể thay thế danh sách: mỗi lần sử dụng, chuyển khóa đến cuối và loại bỏ phần tử ở đầu, vẫn trong O(1). Người phỏng vấn thường muốn bạn tự xây dựng danh sách.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def lruCacheOps(capacity, ops, keys, values):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
capacity = 2 ops = ["put", "put", "get", "put", "get", "get"] keys = [1, 2, 1, 3, 2, 3] values = [10, 20, 0, 30, 0, 0]
Đầu ra mong đợi
["null", "null", "10", "null", "-1", "30"]