Design an LRU Cache
Problem Design and implement a fixed-capacity Least Recently Used cache. When a write would exceed capacity, the least recently used entry is evicted. Both reads and writes must run in O(1).
Requirements
get(key) -> value?— returns the value and marks the entry most recently used; miss returns null/absentput(key, value) -> void— inserts or updates, marks most recently used, evicts the LRU entry when over capacitysize() -> int, and optionallyremove(key)andclear()- Strict O(1) for both
getandput— not amortized-by-scan
Core design
- Two structures working together: a hash map from key -> node pointer for O(1) lookup, and a doubly linked list ordered by recency with most-recently-used at the head and least-recently-used at the tail.
- Neither structure alone suffices — the map gives lookup but no ordering; the list gives ordering but O(n) lookup. The map storing node references is what makes the splice O(1).
get: map lookup -> unlink the node -> push to head -> return value.put: if present, update value and move to head. If absent, create a node at the head and insert into the map; if size > capacity, drop the tail node and delete its key from the map.- The list must be doubly linked: unlinking a node in O(1) requires its predecessor, which a singly linked list cannot give you.
- Sentinel head/tail dummy nodes remove nearly every null check from the splice logic — worth doing, and worth mentioning.
Discussion points
- Eviction must delete from both structures. Forgetting the map delete is the classic leak: the list shrinks but the map grows unbounded.
- Concurrency: the naive fix is one global lock, which serializes every read and makes the cache a bottleneck. Discuss sharding into N independently locked segments, or a concurrent map plus approximate recency (as real caches do) — exact LRU ordering is inherently a contention point.
- Trade-off vs. LFU: LRU is cheap and handles recency-skewed access well but is destroyed by a full scan that evicts the whole working set. LFU resists scans but needs frequency counters with aging, or it ossifies around historically hot keys.
- Alternatives worth naming: a language-provided linked hash map with access-order gives this almost free; segmented LRU and clock/second-chance approximate it more cheaply.
- Edge cases: capacity 0 or 1,
puton an existing key at full capacity (must not evict), and whethergeton a miss should count as an access. - Extension: per-entry TTL, which adds lazy expiry on read or a background sweeper, and interacts with eviction order.
asked …