2
capacity
Design a cache that evicts the Least Recently Used entry when full. The trick: pair a Map for O(1) key lookup with a doubly-linked list for O(1) move-to-front and eviction — both get and put run in constant time.
Implement an LRU cache with a fixed capacity. get(key) returns the value or -1 if absent; put(key, value) inserts or updates the pair. When the cache exceeds capacity, evict the least recently used entry.
Worked example — capacity 2:
put(1,1) → cache: [1:1]put(2,2) → cache: [2:2, 1:1] (2 is MRU)get(1) → 1; cache: [1:1, 2:2] (1 promoted to MRU)put(3,3) → evict key 2 (LRU); cache: [3:3, 1:1]get(2) → -1 (evicted)Map<key, node> gives O(1) lookup, but a plain map has no notion of order. A doubly-linked list gives O(1) move-to-front and O(1) evict-from-tail — but only if you already hold a pointer to the node. Combining them: the map holds the pointer, the list enforces recency order. Add dummy head (MRU side) and tail (LRU side) sentinels to eliminate every null-pointer edge case.Map and two dummy sentinel nodes (head ↔ tail) so the list is never truly empty — every real node lives between them.get(key). Look up the node in the map. If absent return -1. Otherwise call remove(node) then insertFront(node) to promote it to MRU, and return its value.put(key, value) — existing key. Retrieve the node, update its value, then move to front exactly as in get.put(key, value) — new key. Create a new node, insert into the map, call insertFront. If map.size > capacity, evict by calling remove(tail.prev) and deleting its key from the map.remove(node). Splice out: node.prev.next = node.next and node.next.prev = node.prev.insertFront(node). Wire the node right after the dummy head. Four pointer assignments; sentinels ensure no null checks.get— the map is the source of truth for "is key in cache?". Skip the map.delete and stale keys silently return wrong values.JavaScript's built-in Map preserves insertion order, so map.keys().next().value gives the oldest key — this lets you implement LRU without a separate linked list at the cost of code clarity. The doubly-linked list approach is canonical for interviews because it makes the O(1) argument explicit.
2. Dummy head (MRU side) and tail (LRU side) sentinel nodes in place.1class LRUCache {2 private capacity: number;3 private map: Map<number, DLLNode>;4 private head: DLLNode; // dummy MRU sentinel5 private tail: DLLNode; // dummy LRU sentinel67▶ constructor(capacity: number) {8▶ this.capacity = capacity;9▶ this.map = new Map();10▶ this.head = new DLLNode(0, 0);11▶ this.tail = new DLLNode(0, 0);12▶ this.head.next = this.tail;13▶ this.tail.prev = this.head;14 }1516 get(key: number): number {17 if (!this.map.has(key)) return -1;18 const node = this.map.get(key)!;19 this.remove(node);20 this.insertFront(node);21 return node.val;22 }2324 put(key: number, value: number): void {25 if (this.map.has(key)) {26 const node = this.map.get(key)!;27 node.val = value;28 this.remove(node);29 this.insertFront(node);30 } else {31 const node = new DLLNode(key, value);32 this.map.set(key, node);33 this.insertFront(node);34 if (this.map.size > this.capacity) {35 const lru = this.tail.prev!;36 this.remove(lru);37 this.map.delete(lru.key);38 }39 }40 }4142 private remove(node: DLLNode): void {43 node.prev!.next = node.next;44 node.next!.prev = node.prev;45 }4647 private insertFront(node: DLLNode): void {48 node.next = this.head.next;49 node.prev = this.head;50 this.head.next!.prev = node;51 this.head.next = node;52 }53}5455class DLLNode {56 key: number;57 val: number;58 prev: DLLNode | null = null;59 next: DLLNode | null = null;60 constructor(key: number, val: number) {61 this.key = key;62 this.val = val;63 }64}
class LRUCache {
private capacity: number;
private map: Map<number, DLLNode>;
private head: DLLNode; // dummy MRU sentinel
private tail: DLLNode; // dummy LRU sentinel
constructor(capacity: number) {
this.capacity = capacity;
this.map = new Map();
this.head = new DLLNode(0, 0);
this.tail = new DLLNode(0, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key: number): number {
if (!this.map.has(key)) return -1;
const node = this.map.get(key)!;
this.remove(node);
this.insertFront(node);
return node.val;
}
put(key: number, value: number): void {
if (this.map.has(key)) {
const node = this.map.get(key)!;
node.val = value;
this.remove(node);
this.insertFront(node);
} else {
const node = new DLLNode(key, value);
this.map.set(key, node);
this.insertFront(node);
if (this.map.size > this.capacity) {
const lru = this.tail.prev!;
this.remove(lru);
this.map.delete(lru.key);
}
}
}
private remove(node: DLLNode): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private insertFront(node: DLLNode): void {
node.next = this.head.next;
node.prev = this.head;
this.head.next!.prev = node;
this.head.next = node;
}
}
class DLLNode {
key: number;
val: number;
prev: DLLNode | null = null;
next: DLLNode | null = null;
constructor(key: number, val: number) {
this.key = key;
this.val = val;
}
}capacity caps the cache size. The Map maps each key to its doubly-linked list node for O(1) pointer retrieval. Dummy head and tail sentinels avoid null checks: the real entries always live between them, so remove and insertFront never need to handle empty-list edge cases.-1 immediately. On a hit, remove + insertFront promotes the node to the MRU position in O(1). The value is already stored in the node.node.val in place, then re-use the same remove + insertFront dance to refresh recency. No new node is allocated.map.size > capacity, the LRU node is tail.prev: remove it from the list and delete its key from the map. Critically, the map delete must happen after remove so the node's key field is still accessible.remove is four pointer assignments (prev.next and next.prev). insertFront splices the node immediately after the dummy head — also four assignments. Both are O(1) regardless of list length. The sentinels guarantee node.prev and node.next are never null.get and put are both O(1) time: the map gives O(1) pointer lookup; splice-out and splice-in are constant-time pointer rewires. Space is O(n) for n ≤ capacity entries.Map<freq, DoublyLinkedList>; track minFreq to find the eviction bucket in O(1).Mappreserves insertion order; delete-then-reinsert moves a key to the "end" (most recent). But this conflates insertion and access order and is less explicit about the invariant.getand put, or use a lock-free structure like a concurrent skip list.get on a key that was just evicted (must return -1).head.next.key— O(1) because the dummy head's immediate successor is always the MRU node.| "design a cache", evict least recently used | Map + doubly-linked list |
| O(1) ordered delete + O(1) lookup | sentinel DLL + hash map |
| move-to-front on access | remove(node) then insertFront(node) |
| capacity exceeded → evict oldest | remove(tail.prev) + map.delete |
class LRUCache {
private capacity: number;
private map: Map<number, DLLNode>;
private head: DLLNode; // MRU sentinel
private tail: DLLNode; // LRU sentinel
constructor(capacity: number) { /* init map, dummy head/tail */ }
get(key: number): number {
if (!this.map.has(key)) return -1;
// move to front, return val
}
put(key: number, value: number): void {
if (this.map.has(key)) { /* update + move to front */ }
else {
// insert new node at front
// if over capacity, evict tail.prev + delete from map
}
}
private remove(node: DLLNode): void { /* splice out */ }
private insertFront(node: DLLNode): void { /* splice in after head */ }
}put(1,1); put(2,2); get(1); put(3,3); get(2) — what does the last get return?get to return stale data after an eviction?