LRU Cache
Picture
A cache holds A and B. Reading A makes B the least recently used. Adding C must remove B. Fast lookup and fast recency changes need different structures coordinated by one owner.
Predict
Why pair a dictionary with a doubly linked list?
Walk through the small example
The dictionary finds an existing node in expected constant time. The node’s previous and next pointers let the cache detach and reinsert it in constant time without searching for its predecessor.
Mental model and complete Python solution
The dictionary maps keys to their unique nodes. A list runs from least to most recent between two dummy sentinels. A successful get moves its node to the recent end. put updates an existing node or creates one; if capacity is exceeded, evict the least-recent real node from both structures. Every stored key must appear exactly once in the list, and every real node must be indexed. Sentinels simplify edge cases; they are never real entries.
What the function promises: LRUCache(capacity) supports integer-valued get and put as in the linked problem. A miss returns −1. Nonnegative capacity is required; zero capacity stores nothing. This implementation is single-threaded and in memory.
class Link:
def __init__(self, key=None, value=None):
self.key, self.value = key, value
self.previous = self.next = None
class LRUCache:
def __init__(self, capacity):
if type(capacity) is not int or capacity < 0:
raise ValueError("Capacity must be a nonnegative integer")
self.capacity = capacity
self.nodes = {}
self.head, self.tail = Link(), Link()
self.head.next, self.tail.previous = self.tail, self.head
def _detach(self, node):
node.previous.next = node.next
node.next.previous = node.previous
def _make_recent(self, node):
previous = self.tail.previous
previous.next = node
node.previous, node.next = previous, self.tail
self.tail.previous = node
def get(self, key):
if key not in self.nodes:
return -1
node = self.nodes[key]
self._detach(node)
self._make_recent(node)
return node.value
def put(self, key, value):
if self.capacity == 0:
return
if key in self.nodes:
node = self.nodes[key]
node.value = value
self._detach(node)
else:
node = Link(key, value)
self.nodes[key] = node
self._make_recent(node)
if len(self.nodes) > self.capacity:
oldest = self.head.next
self._detach(oldest)
del self.nodes[oldest.key]
cache = LRUCache(2)
cache.put(1, 10)
cache.put(2, 20)
assert cache.get(1) == 10
cache.put(3, 30)
assert cache.get(2) == -1
cache.put(1, 11)
assert cache.get(1) == 11 and len(cache.nodes) == 2
empty = LRUCache(0)
empty.put(1, 1)
assert empty.get(1) == -1
Work and storage: Expected O(1) get and put, O(capacity) space. Python’s OrderedDict can supply the ordering operations in practical code; the explicit list here exposes the invariant being maintained.
When to recognize it: Fast lookup and fast recency updates require different structures kept consistent by one owner.
Check before applying it: A hit changes recency. Updating an existing key must not create a second list node. This example is single-threaded.
Change one rule
Entries also expire after a TTL. Does least-recent order alone identify which entry expires next?
See what changes
Not generally. Recency and expiration are different orders. Decide whether lazy expiry on access is sufficient or add a separate expiry index, then keep all indexes consistent through one update boundary.
Connect
dictionaries: The dictionary locates entries without scanning.
Make failure and retries part of the design: Test operation sequences, not just isolated gets and puts.
Problem reference: LRU Cache (Medium). The walkthrough and Python solution above are original course material.