Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Combine a hash map with a doubly linked list. The map gives O(1) key → node lookup; the doubly linked list keeps nodes in recency order with the most-recently-used near the head and the least-recently-used at the tail. get and put both touch the map (find the node) and the list (move it to the front); when capacity is exceeded, evict the tail. Every operation is O(1). The shortcut answer is Java's LinkedHashMap or Python's OrderedDict, which do this internally.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can compose two data structures so every operation is O(1) — recognising that a hash map alone can't track recency and a list alone can't find in O(1).
Where candidates slip
Using a singly linked list (can't unlink in O(1)), forgetting to move a node to the front on get, or evicting the wrong end.
How to stand out
State the LinkedHashMap/OrderedDict shortcut, then build it from scratch with dummy head/tail sentinels, and cleanly factor out helper methods (addToFront, remove, moveToFront).
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewO(1) get/put cache with LRU evictionAllowed to use a library structure?Yes (production)No (interview)Explore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Built-in Ordered Map (LinkedHashMap / OrderedDict)
1Hint 1Explore hintHide hint
Some standard libraries already maintain a hash map whose keys remember their access or insertion order. What would that give you for free?
2Hint 2Explore hintHide hint
Java's LinkedHashMap has an access-order mode and a removeEldestEntry hook; Python's OrderedDict has move_to_end and popitem(last=False).
3Hint 3Explore hintHide hint
On get: fetch and move the key to the most-recent end. On put: insert/refresh, then if size exceeds capacity pop the least-recent end.
A · Core idea
The shortest correct answer reuses a library structure that is a hash map backed by a linked list.
Java — LinkedHashMap in access-order mode. Constructing it with new LinkedHashMap<>(capacity, 0.75f, true) makes it reorder entries on every access (get/put) so the eldest entry is the least recently used. Overriding removeEldestEntry to return size() > capacity lets the map evict automatically on insert. get returns the value or -1.
Python — OrderedDict. move_to_end(key) marks a key most-recently-used; popitem(last=False) removes the least-recently-used (front). On get we move the key to the end and return its value; on put we overwrite, move to end, and pop the front if we're over capacity.
Why mention it at all. In real code this is the right answer — fewer lines, fewer bugs. But interviewers almost always follow with 'now implement it without that' because the point of the question is whether you understand the underlying map + doubly linked list. Naming the shortcut shows language fluency; the from-scratch build shows data-structure fluency.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
capacity=2; put(1,1); put(2,2); get(1); put(3,3)put(1,1)
insert 1
{1:1}key 2 evicted; cache = {1:1, 3:3}C · Code with teaching comments
Reference implementation previewimport java.util.LinkedHashMap; import java.util.Map; public class LRUCache { private final int capacity;Explore the full codeHide the full code
D · Why it works and complexityO(1) timeO(capacity) spaceOpen the invariant and proofExplore the proofHide the proof
Recognising that LinkedHashMap / OrderedDict already encapsulate exactly the map + doubly-linked-list pairing is a sign you understand the structure — it's the same machine, just pre-assembled. Lead with it, but be ready to open the hood.
ComplexityO(1) time, O(capacity) space — These structures are themselves a hash map plus a linked list, so each operation is O(1) average. They hold at most capacity entries → O(capacity) space.
E · Edge cases and pitfalls3Forgetting the access-order flag (true) on LinkedHashMap — then it tracks insertion order, not recency.Explore edge casesHide edge cases
Pitfalls
- Forgetting the access-order flag (true) on LinkedHashMap — then it tracks insertion order, not recency.
- In Python, not calling move_to_end on get, so recency isn't refreshed on reads.
- Relying on this in an interview that explicitly asks you to implement it manually.
Approach 2 of 2 · Optimal
HashMap + Doubly Linked List (Optimal, From Scratch)
1Hint 1Explore hintHide hint
A hash map gives O(1) lookup but no notion of order. A list gives order but O(n) lookup. What if the map's values were the list's nodes, so you get both?
2Hint 2Explore hintHide hint
Use a doubly linked list so any node can be unlinked in O(1) (you need its prev). Keep most-recently-used at the front, least-recently-used at the back.
3Hint 3Explore hintHide hint
Add dummy head and tail nodes so insertion and removal never special-case the ends. On get/put, remove the node then re-add it at the front; on overflow, remove the node before the tail and delete its map entry.
A · Core idea
We pair two structures so each covers the other's weakness: the map finds any key's node in O(1), and the doubly linked list reorders and evicts in O(1).
The data model. Each list node stores key, value, prev, next. The hash map maps key → node. We keep two sentinels: a dummy head and dummy tail. The node right after head is the most recently used; the node right before tail is the least recently used. Sentinels mean every real node always has non-null neighbours, so insert/remove need no null checks.
get(key). If the key isn't in the map, return -1. Otherwise grab the node, unlink it from its current position, re-insert it just after head (now it's most-recently-used), and return its value. Both the unlink and the re-insert are constant-time pointer updates.
put(key, value). If the key exists, update the node's value and move it to the front. If it's new, create a node, put it in the map, and insert it at the front. Then, if the map's size exceeds capacity, evict: take the node just before tail (the LRU), unlink it, and delete its entry from the map — forgetting the map deletion is the classic leak that makes the structure grow unbounded.
Why doubly linked, why sentinels. A singly linked list can't remove an arbitrary node in O(1) because you can't reach its predecessor — you'd scan, making it O(n). The prev pointer fixes that. Dummy head/tail remove the 'is this the first/last node?' branches, so addToFront and remove are three or four unconditional pointer writes each.
Helper methods pay off. Factoring remove(node) and addToFront(node) keeps get and put short and makes 'move to front' literally remove(node); addToFront(node). Clean helpers are also what an interviewer reads to confirm your pointer surgery is correct.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
capacity=2; put(1,1); put(2,2); get(1); put(3,3); get(2)Array state
before
1 is MRU after a get(1)
get(1)=1, get(2)=-1; key 2 evictedC · Code with teaching comments
D · Why it works and complexityO(1) timeO(capacity) spaceOpen the invariant and proofExplore the proofHide the proof
This is the canonical 'compose two structures around one invariant' design. The map's value being the list node (not the raw value) is the linchpin — it's what lets you jump from a key straight to its position in the recency order and splice it in O(1). Internalise this and LFU Cache and All O(1) Data Structure are variations on the theme.
ComplexityO(1) time, O(capacity) space — Map lookup is O(1); splicing a node out and re-inserting at the front is O(1) thanks to the doubly linked list and sentinels. The map and list each hold at most capacity nodes → O(capacity) space.
E · Edge cases and pitfalls7capacity = 1; put(1,1); put(2,2) — put(2,2) adds node2 then size 2 > 1 → evicts node1; get(1) returns -1.Explore edge casesHide edge cases
Edge cases handled
capacity = 1; put(1,1); put(2,2)put(2,2) adds node2 then size 2 > 1 → evicts node1; get(1) returns -1.put on existing keyValue updated and node moved to front; map size unchanged, so no eviction.get on missing keymap.get returns null → return -1; list untouched.
Pitfalls
- Using a singly linked list — removing an arbitrary node becomes O(n) without a prev pointer.
- Evicting but forgetting map.remove(lru.key) — the map grows unbounded (memory leak).
- Not moving the node to the front on get, so recency stops reflecting reads.
- Skipping dummy sentinels and crashing on inserts/removes at the list ends.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewimport java.util.HashMap; import java.util.Map; public class LRUCache { private static class Node {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Evicting from the list but not the map
If you unlink the LRU node but leave its key in the map, the map keeps growing and stale keys still 'hit', breaking correctness and leaking memory.
Explore the code comparisonHide the code comparison
Evicting from the list but not the map
If you unlink the LRU node but leave its key in the map, the map keeps growing and stale keys still 'hit', breaking correctness and leaking memory.
Don't write this
if len(self.map) > self.capacity:
lru = self.tail.prev
self._remove(lru)
# forgot: del self.map[lru.key]Write this instead
if len(self.map) > self.capacity:
lru = self.tail.prev
self._remove(lru)
del self.map[lru.key]2Not refreshing recency on get
A get is a use. If you return the value without moving the node to the front, the LRU order becomes wrong and you evict still-hot keys.
Explore the code comparisonHide the code comparison
Not refreshing recency on get
A get is a use. If you return the value without moving the node to the front, the LRU order becomes wrong and you evict still-hot keys.
Don't write this
public int get(int key) {
Node node = map.get(key);
if (node == null) return -1;
return node.value; // recency not updated!
}Write this instead
public int get(int key) {
Node node = map.get(key);
if (node == null) return -1;
remove(node);
addToFront(node);
return node.value;
}Revise
Carry the pattern into the interview
Pattern to remember
Hash map (key → node) + doubly linked list ordered by recency
Code pattern to memorize
get(key): if miss return -1; move node to front; return val
put(key,val): if exists update + move to front; else add node at front;
if size > capacity: remove tail node and its map entryRules to remember
- 1Use a doubly linked list — you must unlink a node in O(1), which needs its
prevpointer. - 2Keep dummy head and tail sentinels so insert/remove never hit a null neighbour edge case.
- 3Most-recently-used lives next to head; least-recently-used sits just before tail — evict from the tail.
- 4Both
getandput(even on an update) count as a use → move the node to the front. - 5The map stores key → node reference, not key → value, so you can splice the node out of the list in O(1).
Use this pattern when
- Design a fixed-capacity cache with O(1) get/put and least-recently-used eviction
- Any 'O(1) access + ordered by recency/insertion' requirement
- Implementing an LFU/LRU layer, a bounded memo table, or a page-replacement policy
- When you need both fast lookup AND fast reordering of the same elements
Choose another pattern when
- Eviction is by frequency, not recency → LFU Cache (counts + buckets), a different structure
- No capacity bound / no eviction → a plain hash map suffices
- You only append and never reorder → a singly linked list or queue is enough
Interview takeaway
The hash-map-plus-doubly-linked-list combo is the archetypal 'two data structures, one invariant' design: the map buys O(1) find, the list buys O(1) reorder/evict. Master the dummy-sentinel splicing and LFU Cache and All O(1) Data Structure follow the same blueprint.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternThe hash-map-plus-doubly-linked-list design: a map for O(1) lookup married to a doubly linked list for O(1) reorder/evict, kept consistent by storing nodes as the map's values. The blueprint behind LFU Cache and All O(1) Data Structure.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- LFU CacheComing soonEvict by least frequency instead of recency — keep a frequency map of doubly linked lists, one per count.
- All O`one Data StructureComing soonSame map + doubly linked list of buckets to keep min/max key counts in O(1).
- First Unique NumberComing soonHash map for counts plus an ordered structure to pop the first unique in O(1).