LRU Cache
Design an LRU cache with O(1) get and put — Map + doubly linked list, Map insertion-order approach, and interview tradeoffs.
- dsa
- design
- hashmap
- linked-list
- Meta
- Amazon
- Uber
The problem
Design a data structure that follows Least Recently Used eviction:
get(key)→ return value or-1if missing; counts as use (moves to most-recent).put(key, value)→ insert or update; counts as use; if capacity exceeded, evict the least recently used key.
Both operations should be O(1) average time.
capacity = 2
put(1, 1)
put(2, 2)
get(1) // 1 (1 is now most recent)
put(3, 3) // evicts key 2
get(2) // -1
put(4, 4) // evicts key 1
get(1) // -1
get(3) // 3
get(4) // 4
Why interviewers love this: hash map alone is O(1) lookup but not O(1) “find oldest”; list alone is ordered but slow lookup. You need both.
Frontend cousins: in-memory request caches, image decode caches, editor undo windows with capped size.
Approach A — Map insertion order (practical JS)
JavaScript Map iterates in insertion order. If you delete + re-set on every access, the key moves to the end (most recent). The first key is least recent.
class LRUCache {
private capacity: number;
private map = new Map<number, number>();
constructor(capacity: number) {
this.capacity = capacity;
}
get(key: number): number {
if (!this.map.has(key)) return -1;
const value = this.map.get(key)!;
this.map.delete(key);
this.map.set(key, value); // move to most-recent
return value;
}
put(key: number, value: number): void {
if (this.map.has(key)) {
this.map.delete(key);
} else if (this.map.size >= this.capacity) {
// first key = least recently used
const lruKey = this.map.keys().next().value as number;
this.map.delete(lruKey);
}
this.map.set(key, value);
}
}
| Time | O(1) amortized per op (engine Map) |
| Space | O(capacity) |
Interview note: This is valid in JS/TS interviews when you explain why order updates work. Some interviewers still want the classic DLL for language-agnostic design.
Approach B — Hash map + doubly linked list (classic)
- Map:
key → node - DLL: head = dummy before MRU, tail = dummy after LRU (or the reverse — pick one and stick to it)
On get/put hit: splice node out, insert after head (MRU).
On capacity overflow: remove node before tail (LRU), delete from map.
class Node {
key: number;
value: number;
prev: Node | null = null;
next: Node | null = null;
constructor(key = 0, value = 0) {
this.key = key;
this.value = value;
}
}
class LRUCache {
private capacity: number;
private map = new Map<number, Node>();
private head = new Node(); // MRU side
private tail = new Node(); // LRU side
constructor(capacity: number) {
this.capacity = capacity;
this.head.next = this.tail;
this.tail.prev = this.head;
}
private remove(node: Node): void {
const p = node.prev!;
const n = node.next!;
p.next = n;
n.prev = p;
}
private insertAfterHead(node: Node): void {
const first = this.head.next!;
node.prev = this.head;
node.next = first;
this.head.next = node;
first.prev = node;
}
get(key: number): number {
const node = this.map.get(key);
if (!node) return -1;
this.remove(node);
this.insertAfterHead(node);
return node.value;
}
put(key: number, value: number): void {
const existing = this.map.get(key);
if (existing) {
existing.value = value;
this.remove(existing);
this.insertAfterHead(existing);
return;
}
const node = new Node(key, value);
this.map.set(key, node);
this.insertAfterHead(node);
if (this.map.size > this.capacity) {
const lru = this.tail.prev!; // real LRU node
this.remove(lru);
this.map.delete(lru.key);
}
}
}
| Time | O(1) per get/put |
| Space | O(capacity) |
Dummy head/tail avoid null checks on edges. Practice drawing the pointers once; the code is mechanical after that.
Complexity proof points (say them)
- Hash map get/set/delete → O(1) average
- DLL remove/insert given a node pointer → O(1)
- Finding LRU without a list would be O(n) scan of timestamps — that’s the fail path
Edge cases
capacity = 1— every new key evicts the oldputsame key — update value + refresh recency, no evictiongetmiss — must not mutate order- Negative keys/values — fine unless constraints say otherwise
Common mistakes
- Updating value without moving the node to MRU
- Evicting MRU instead of LRU (pointer confusion)
- Forgetting to delete from both map and list
- Using an array +
indexOf(O(n)) and calling it O(1) - In Map version:
setwithoutdeletefirst — does not refresh order in JS Map
Follow-ups
- LFU (least frequently used) — harder; needs freq buckets
- TTL + LRU — real caches; eviction by time and recency
- Thread safety — locks / concurrent maps (backend interviews)
- size in bytes not count — weight-based eviction
Interview delivery
- Clarify capacity ≥ 1, return
-1on miss, update refreshes recency. - Reject array-only design.
- Propose map + DLL or JS Map order; implement one fully.
- Trace the sample.
- State complexities.
Ship working code over perfect abstraction. Interviewers watch pointer bugs more than class naming.
Related
- Two Sum — hash map fundamentals
- Time Based Key Value Store
- Min Stack
- Design Twitter Lite
- Meta Frontend Interview — design + coding mix
- Debounce Implementation — real client-side “don’t stampede” caching neighbor