LRU Cache
Fixed-capacity cache: get and put in O(1), evicting the least recently used key when full.
Intuition
You need O(1) lookup (hash map) AND O(1) reordering by recency (doubly linked list). Marry them: map key → list node; every touch unlinks the node and reinserts at the front; eviction pops the tail. Neither structure alone can do both jobs.
Whenever a data-structure question demands O(1) for operations that seem to need ordering, the answer is usually two structures glued together, each covering the other's weakness. A hash map finds any key instantly but knows nothing about recency; a doubly linked list maintains order and splices in O(1) but can't search. Store the node as the map's value and you get both — the map hands you the node, and the node already knows its neighbours.
Approach
Why a doubly linked list
Removing a node from the middle in O(1) needs prev and next pointers — recency order is exactly 'move to front on touch'.
Sentinels kill edge cases
Dummy head and tail mean every real node has real neighbours — no null checks on unlink/insert.
The two ops
get: look up, move node to front. put: update-or-create at front; if over capacity, unlink tail.prev and delete its map entry.
Solution & live demo
Common pitfalls
Using a singly linked list
class Node:
def __init__(self, k, v):
self.key, self.val, self.next = k, v, Noneclass Node:
def __init__(self, k, v):
self.key, self.val = k, v
self.prev = self.next = NoneUnlinking a node needs its predecessor. With only forward pointers you must walk from the head to find it, making every get O(n) and destroying the whole point. The backward pointer is what makes removal O(1).
Storing only the value in the map
self.map[key] = value
self.map[key] = node
The map's job isn't just to return the value — it's to locate the node so it can be spliced to the front. Keeping the raw value means searching the list for it, which is O(n).
Not storing the key inside the node
class Node:
def __init__(self, v):
self.val = vclass Node:
def __init__(self, k, v):
self.key, self.val = k, vOn eviction you find the least-recent node from the list tail, but you must also delete its entry from the map — and that needs the key. Without a back-reference the map keeps a dead entry forever and the cache leaks.
Edge cases
Update value and refresh recency — must NOT evict.
Every distinct put evicts the previous key; sentinels keep the list sane.