The problem. Design a Least Recently Used (LRU) cache with a fixed capacity. get(key) returns the value if the key is cached, otherwise -1. put(key, value) adds or updates a key; if that makes the cache hold more than capacity keys, it evicts the key used least recently. Both operations must run in O(1) average time. Using a key — get or put — makes it the most recently used.
LRUCache(2)
put(1, 1), put(2, 2)
get(1) -> 1 (1 is now the most recent)
put(3, 3) (full: evicts 2, the least recent)
get(2) -> -1
put(4, 4) (evicts 1)
get(1) -> -1
get(3) -> 3
get(4) -> 4The cache needs two things at once. It must find a key's entry in O(1) — that's a hash map. And it must keep entries in order of use, so that it can move any entry to the front and remove the oldest from the back, also in O(1) — that's a doubly linked list. An array can't move an item from the middle to the front without shifting; a singly linked list can't unlink a node without first finding the node before it. A doubly linked node knows both neighbours, so moving or removing it is a few pointer changes.
The map stores key → node, so get jumps straight to the node, unlinks it and re-adds it at the front. Two sentinel nodes, head and tail, sit at the ends permanently: the list is never truly empty, so unlinking and adding never need a null check. The least recently used entry is always tail.prev.
class LRUCache { constructor(capacity) { this.capacity = capacity; this.values = new Map(); // key -> value this.order = []; // keys, least recently used first } touch(key) { // move key to the most recent end: O(n) const i = this.order.indexOf(key); if (i >= 0) this.order.splice(i, 1); this.order.push(key); } get(key) { if (!this.values.has(key)) return -1; this.touch(key); return this.values.get(key); } put(key, value) { this.values.set(key, value); this.touch(key); if (this.order.length > this.capacity) { const lru = this.order.shift(); // the front of the array is the least recent this.values.delete(lru); } }}Time O(capacity) per operation, space O(capacity). Finding the key in the order array and shifting the array after removing it are both linear. Correct, but not the O(1) the question asks for.
The map finds the node; the list moves it to the front or drops the one before tail.
class Entry { constructor(key, value) { this.key = key; this.value = value; this.prev = null; this.next = null; }} class LRUCache { constructor(capacity) { this.capacity = capacity; this.map = new Map(); // key -> its node, so any entry is found in O(1) this.head = new Entry(0, 0); // sentinels: head.next is the most recently used, this.tail = new Entry(0, 0); // tail.prev the least recently used this.head.next = this.tail; this.tail.prev = this.head; } remove(node) { // unlink a node in O(1) node.prev.next = node.next; node.next.prev = node.prev; } addFront(node) { // insert right after the head sentinel node.prev = this.head; node.next = this.head.next; this.head.next.prev = node; this.head.next = node; } get(key) { const node = this.map.get(key); if (node === undefined) return -1; this.remove(node); // it was just used: move it to the front this.addFront(node); return node.value; } put(key, value) { if (this.map.has(key)) this.remove(this.map.get(key)); const node = new Entry(key, value); this.map.set(key, node); this.addFront(node); if (this.map.size > this.capacity) { const lru = this.tail.prev; // the least recently used is at the back this.remove(lru); this.map.delete(lru.key); } }}Time O(1) per operation, space O(capacity). Each operation is a hash lookup plus a constant number of pointer changes. The node stores its key as well as its value so that, when it's evicted from the back of the list, the map entry can be deleted too.
Most languages ship this combination: JavaScript's Map remembers insertion order (delete and re-set a key to refresh it), Python has collections.OrderedDict with move_to_end, and Java's LinkedHashMap takes an access-order flag and a removeEldestEntry hook. Each is a hash map threaded through a doubly linked list — the interview wants you to build it yourself.
LRU Cache is one of the most asked design questions of all, because it combines two data structures so that each covers the other's weakness.
get(1), key 1 must not be the next evicted.