Algorithm

LRU Cache

Design Pattern

LRU Cache

Design a data structure that follows the constraints of a Least Recently Used (LRU) cache. LRUCache(int capacity) initializes the cache. int get(int key) returns the value if it exists, otherwise -1. void put(int key, int value) updates or inserts the key. If the number of keys exceeds capacity, evict the least recently used key. Both get and put must run in O(1) average time.

CONSTRAINTS
  • 1 <= capacity <= 3000
  • 0 <= key <= 10⁴
  • 0 <= value <= 10⁵
  • At most 2 × 10⁵ calls will be made to get and put
EXAMPLE 1
Input: ["LRUCache","put","put","get","put","get","put","get","get","get"] [[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]
Output: [null, null, null, 1, null, -1, null, -1, 3, 4]
Capacity 2. put(1,1), put(2,2) fill it. get(1)→1 refreshes key 1, so key 2 is now stalest. put(3,3) evicts 2, so get(2)→-1. put(4,4) evicts the now-stalest key 1, so get(1)→-1. Keys 3 and 4 remain: get(3)→3, get(4)→4.
EXAMPLE 2
Input: ["LRUCache","put","put","put","get","get"] [[2],[1,10],[2,20],[1,11],[1],[2]]
Output: [null, null, null, null, 11, 20]
Capacity 2. put(1,10) then put(2,20) fill the cache. put(1,11) updates an existing key: it overwrites the value to 11 and refreshes key 1 — the size stays 2, so nothing is evicted. get(1)→11 (the updated value); get(2)→20 (still present).
Does put on an existing key add a new entry?
No — it updates the value in place and marks the key as most-recently-used. The cache size doesn't grow, so no eviction is triggered by an update.
Why a doubly linked list and not singly?
Removing a known node in O(1) requires rewiring the node before it. Only a doubly linked list lets a node reach its predecessor directly via a prev pointer; a singly linked list would need an O(N) walk to find it.
What does get return for a missing key, and does a miss change anything?
It returns -1 and leaves the cache untouched — a miss is not a use, so no recency changes.
Is thread-safety expected?
Interviews usually assume single-threaded access. In production you'd guard the map and list with a lock or use concurrent structures, but that's outside the core algorithm.

A cache needs to be two things: fast at finding data and strict about throwing it away. The challenge of an LRU (Least Recently Used) cache is that we need to perform three operations in constant time: finding an existing item, updating its "freshness," and evicting the oldest item when full.

The Bottleneck: The Trade-off

If we use a Hash Map, we get O(1) lookups, but we have no way of knowing which item is the "oldest" without scanning everything. If we use an Array or a Linked List, we can maintain order, but finding a specific key in the middle requires a linear scan.

python
# Naive approach using a simple list
cache = [] # List of [key, value]
def get(key):
    for i, (k, v) in enumerate(cache):
        if k == key:
            # Move to front (MRU)
            item = cache.pop(i)
            cache.insert(0, item)
            return v
    return -1 # O(N) scan
The Dual-Structure Hybrid

To get the best of both worlds, we combine them. We use a Hash Map to store key -> node pointers and a Doubly Linked List (DLL) to store the actual data.
- The Map gives us instant O(1) access to any node in the DLL.
- The DLL allows us to "splice" a node out of its current position and move it to the front in O(1) time, because we have direct access to its prev and next neighbors.

The Baseline: Safety Sentinels

To avoid messy "null" checks (e.g., when the cache is empty or has one item), we use Dummy Head and Dummy Tail nodes.
- Head.next always points to the Most Recently Used (MRU) item.
- Tail.prev always points to the Least Recently Used (LRU) item.
These sentinels never move, acting as permanent anchors for our "line" of data.

The Surgery: Promotion & Eviction

Every time a key is accessed (get) or updated (put), we perform "surgery" on the DLL:
1. Remove: We disconnect the node from its neighbors (node.prev.next = node.next).
2. Promote: We insert it immediately after the Dummy Head.
3. Evict: If we exceed capacity, we remove the node at Tail.prev and delete its key from the Map.

python
def _remove(node):
    # Surgery: stitch the neighbors together
    p, n = node.prev, node.next
    p.next, n.prev = n, p

def _add_to_head(node):
    # Insert between Dummy Head and the old first node
    first = head.next
    head.next = node
    node.prev = head
    node.next = first
    first.prev = node

def get(key):
    if key not in cache_map: return -1
    node = cache_map[key]
    _remove(node)      # Take it out
    _add_to_head(node) # Put it at the front
    return node.val
Worked Example:LRU Trace (Cap=2)
0
1
LRU (old)
1
2
MRU (new)
put(1, A), put(2, B): Cache is full. Key 1 is Least Recently Used, Key 2 is Most Recently Used.
0
2
LRU
1
1
MRU
get(1): Key 1 is accessed, so it gets promoted to MRU. Key 2 is now LRU.
0
1
LRU
1
3
MRU
put(3, C): Cache full. Evict LRU (Key 2). Add Key 3 as MRU. Key 1 becomes LRU. Map = {1: A, 3: C}.
0
1
1
3
get(2): Key 2 was evicted, returns -1. No change to cache order.
0
3
LRU
1
4
MRU
put(4, D): Cache full. Evict LRU (Key 1). Add Key 4 as MRU. Key 3 becomes LRU. Map = {3: C, 4: D}.
Interactive Strategy Visualization
CACHING ARCHITECTURE

Dual-Structure O(1) Cache

INITIALIZING
Most Recent (Head)Least Recent (Tail)
QUEUE EMPTY
O(1) ACCESS

Mental Model

  • Hash Map: Stores `key -> node` for constant time lookups.
  • DLL: Maintains usage order. `Head` is fresh, `Tail` is stale.
Strategy TraceSTEP 1/7
LRU Cache: Least Recently Used eviction policy ensures fixed-size memory management.
HINT

O(1) Everything

By combining these two structures, we get the best of both worlds: instant lookup from the Map and instant ordering updates from the Linked List.

O(N) Scan for Stalest
O(1) Hash Map + Doubly Linked List