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 is a small fast store with a fixed capacity. When it fills up and a new item arrives, something must be thrown out — and Least Recently Used is the eviction rule: kick out whatever hasn't been touched in the longest time. We need three things, all in O(1): get(key) returns a value (or -1) and marks that key as freshly used; put(key, value) inserts or updates and marks it fresh; and when we're over capacity, instantly find and drop the stalest key. The hard part is that "recently used" is an ordering that changes on every single access.

Two structures, opposite strengths

Think about what one structure alone gives you. A hash map finds any key in O(1) — but it has no notion of order, so it can't tell you which key is stalest without scanning all of them (O(N)). A list ordered by recency knows instantly who's stalest (it's at one end) — but finding a specific key inside it means a linear scan (O(N)). Either way, one of the two operations collapses to O(N). This is the same conflict you resolved in Insert Delete GetRandom: two operations demanding structures that pull apart. The resolution is the same — use both, and keep them in sync.

The pairing: hash map + doubly linked list

Keep the values in a doubly linked list ordered by recency: most-recently-used at the front, least-recently-used at the back. Alongside it, a hash map stores key → the node holding that key (not the value — the actual node object). Now a get is two O(1) steps: the map jumps straight to the node, and because it's a doubly linked list, that node knows both its neighbors, so we can splice it out and move it to the front without walking anything. Eviction is trivial: the stalest key is always the node just before the back.

Why doubly linked, not singly? To remove a node in O(1) you must rewire the node before it — and only a doubly linked list lets a node reach its predecessor directly via a prev pointer. A singly linked list would force an O(N) walk to find that predecessor, killing the whole point. (This is exactly the "hand me a node reference and I'll delete it cheaply" case flagged back in Design Linked List.)

Sentinels kill the edge cases

Just like the dummy node in Design Linked List — but now two of them, a permanent head and tail that hold no data and never move. Real nodes always live between them. Because every real node is guaranteed a neighbor on both sides, splicing and inserting need no "is this the first/last node?" checks — the code is one uniform path.

python
class Node:
    def __init__(self, key, val):
        self.key, self.val = key, val
        self.prev = self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.map = {}                     # key -> Node
        self.head, self.tail = Node(0, 0), Node(0, 0)   # sentinels
        self.head.next, self.tail.prev = self.tail, self.head

    def _remove(self, node):              # unstitch from current position
        node.prev.next, node.next.prev = node.next, node.prev

    def _add_front(self, node):           # insert right after head (most recent)
        first = self.head.next
        node.prev, node.next = self.head, first
        self.head.next = first.prev = node

    def get(self, key):
        if key not in self.map:
            return -1
        node = self.map[key]
        self._remove(node)                # pull it out...
        self._add_front(node)             # ...and mark it freshest
        return node.val

    def put(self, key, value):
        if key in self.map:               # update: refresh position
            self._remove(self.map[key])
        node = Node(key, value)
        self.map[key] = node
        self._add_front(node)
        if len(self.map) > self.cap:      # over capacity → evict stalest
            lru = self.tail.prev
            self._remove(lru)
            del self.map[lru.key]         # map deletion needs the node's key
Crucial Notestore key → node in the map, and give each node its own key field. When you evict, you find the stale node from the list (tail.prev) but must also delete it from the map — and the map is keyed by key, not by node. Without node.key you'd have the node in hand but no way to find its map entry, leaking it forever. The node carrying its own key is what closes the loop between the two structures.
Worked Example:a refresh, an eviction, and a miss (capacity 2)
List shown front→back, between the sentinels.
- put(1, A) → list [1], map {1}.
- put(2, B) → 2 is freshest: list [2, 1], map {1, 2}.
- get(1) → hit. Splice node 1 out and to the front: list [1, 2]. Return A. Note 2 is now the stale one.
- put(3, C) → new key, add to front: list [3, 1, 2], size 3 > 2. Evict tail.prev = node 2: list [3, 1], map {1, 3}.
- get(2) (the non-happy path) → 2 was just evicted, not in the map → return -1. The earlier get(1) is exactly what saved key 1 from being the one dropped.
What it costs, and the reflex

Both get and put are O(1): one map lookup plus a constant number of pointer rewires, and eviction reads a fixed position. Memory is O(capacity) for the map and the list together. The transferable move is the whole design: pair a hash map (find any item fast) with an ordered linked list (know the extreme fast), and let each node carry the key that ties them together. That template — fast lookup married to fast ordering — is the backbone of nearly every cache. The very next problem, LFU Cache, stretches it one dimension further: instead of a single recency list, a whole family of lists, one per access-frequency.

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