LRU Cache
LRU Cache: fixed-capacity cache: get and put in O(1), evicting the least recently used key when full.
- 1 <= capacity <= 3000
- 0 <= key <= 10⁴
- 0 <= value <= 10⁵
- At most 2 * 10⁵ calls will be made to get and put.
Intuition
An lru cache holds a fixed number of keys and evicts the least recently used one when full. Both get and put must run in O(1), and that requirement is what forces the design.
Two separate jobs need doing, and no single structure does both:
- Find a key instantly — a hash map does this, but has no notion of order.
- Reorder by recency instantly — a list has order, but finding a key in it is O(n).
So combine them. The hash map stores key to node reference, and the nodes live in a doubly linked list ordered by recency. Looking up a key gives you the node directly; because the node knows its own neighbours, unlinking it and moving it to the front is O(1) — no scanning.
The list must be doubly linked. Removing a node from the middle requires rewiring the node before it, and only a prev pointer makes that node reachable in constant time. A singly linked list would force a scan to find the predecessor, which is exactly the O(n) you are trying to avoid.
One implementation detail removes most of the bugs: dummy head and tail sentinels. With them, every real node always has a real neighbour on both sides, so unlinking and inserting need no null checks and the empty-cache case behaves like every other.
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
Before reading on: price up what the direct approach costs here, then ask which pointers must be saved before you overwrite anything. Aim for O(1) per op time and O(capacity) space.
Understand why one structure is not enough
A hash map gives O(1) lookup but no ordering; a list gives ordering but O(n) lookup. Pairing them lets each do the job it is good at — the map finds the node, the list orders it.
Map keys to list nodes
The hash map stores key to a reference to the node itself, not to the value. That reference is what makes reordering O(1): you arrive at the node without traversing anything.
Use a doubly linked list for recency
Front means most recently used, back means least. The prev pointer is essential — unlinking a middle node requires reaching its predecessor, and a singly linked list would need an O(n) scan to find it.
Add dummy head and tail sentinels
Allocate permanent nodes at both ends. Every real node then has real neighbours, so unlink and insert are branch-free. This removes the empty-list and single-element special cases that cause most of the bugs in this problem.
Implement get as lookup plus move
Find the node through the map, unlink it from its current position, and reinsert it at the front. Return its value. If the key is absent, return −1 without touching the list.
Implement put as upsert plus evict
If the key exists, update its value and move it to the front. Otherwise create a node, insert at the front, and add it to the map. If the size now exceeds capacity, unlink the node before the tail sentinel and delete its map entry — deleting from only one of the two structures is the classic leak.
Cost of the design
Both operations do a constant number of hash lookups and pointer rewires, so both are O(1) time. Space is O(capacity) for the map and the list together.
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.