Reasoning Into CodeArunank Divya Sharan · Python edition← Arunank’s portfolio
Module 25 · Bit Manipulation, Mathematics, and Designed Data Structures

249. Design LRU Cache

Interview · Download this solution · All solutions and shared helpers

Problem

Implement an LRU cache with nonnegative capacity, integer keys and values, get(key), and put(key,value). get returns -1 for a miss; a hit becomes most recent. put returns None, updates or inserts as most recent, and evicts the least recent key when over capacity. Capacity zero stores nothing.

Examples

Example 1

run_operations(LRUCache,[("put",1,10),("put",2,20),("get",1),("put",3,30),("get",2),("get",3)],2)
=> [None, None, 10, None, -1, 30]

Reading key 1 makes key 2 the eviction victim.

Example 2

run_operations(LRUCache,[("put",1,10),("put",1,11),("get",1)],1)
=> [None, None, 11]

Updating an existing key replaces its value without adding a duplicate node.

Example 3

run_operations(LRUCache,[("put",1,10),("get",1)],0)
=> [None, -1]

A zero-capacity cache immediately evicts an insertion.

Reveal the worked solution and reasoning

Why this mechanism works

A dictionary finds an entry immediately, while a doubly linked list tracks recency and supports removing that known entry without searching. Sentinel nodes avoid separate empty/head/tail cases. Keep the least recent real node after head and the most recent before tail. Hits and updates move their nodes to that end.

State and boundaries

nodes maps exactly cached keys to their list nodes. The list contains each cached node once, ordered from least to most recent, with mutually consistent prev/next links.

Protocol

  1. Initialize empty linked sentinels and map.
  2. On a hit detach the node and append it before tail.
  3. On put update an existing node or allocate a new one at the recent end.
  4. If capacity is exceeded, detach head.next and delete its map entry.

Visualization

head <-> least recent <-> ... <-> most recent <-> tail
get(key): detach its node, append before tail
overflow: remove head.next from both list and dictionary

Python implementation

from structures import run_operations

class CacheNode:
    def __init__(self, key=None, value=None):
        self.key, self.value = key, value
        self.prev = self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.capacity, self.nodes = capacity, {}
        self.head, self.tail = CacheNode(), CacheNode()
        self.head.next, self.tail.prev = self.tail, self.head

    def _detach(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _append_recent(self, node):
        previous = self.tail.prev
        previous.next, node.prev = node, previous
        node.next, self.tail.prev = self.tail, node

    def get(self, key):
        if key not in self.nodes:
            return -1
        node = self.nodes[key]
        self._detach(node)
        self._append_recent(node)
        return node.value

    def put(self, key, value):
        if key in self.nodes:
            node = self.nodes[key]
            node.value = value
            self._detach(node)
        else:
            node = CacheNode(key, value)
            self.nodes[key] = node
        self._append_recent(node)
        if len(self.nodes) > self.capacity:
            oldest = self.head.next
            self._detach(oldest)
            del self.nodes[oldest.key]

Worked execution

Recorded state changes after statements; L numbers refer to the solution file. Long traces are explicitly condensed.

run_operations(LRUCache,[("put",1,10),("put",2,20),("get",1),("put",3,30),("get",2),("get",3)],2)

L10 __init__: self.capacity, self.nodes = capacity, {}
self=LRUCache{'capacity':2, 'nodes':{}}; capacity=2

L5 __init__: self.key, self.value = key, value
self=CacheNode{'key':None, 'value':None}; key=None; value=None

L6 __init__: self.prev = self.next = None
self=CacheNode{'key':None, 'value':None, 'prev':None, 'next':None}; key=None; value=None

L5 __init__: self.key, self.value = key, value
self=CacheNode{'key':None, 'value':None}; key=None; value=None

L6 __init__: self.prev = self.next = None
self=CacheNode{'key':None, 'value':None, 'prev':None, 'next':None}; key=None; value=None

L11 __init__: self.head, self.tail = CacheNode(), CacheNode()
self=LRUCache{'capacity':2, 'nodes':{}, 'head':CacheNode..., 'tail':CacheNode...}; capacity=2

L32 put: if key in self.nodes:
self=LRUCache{'capacity':2, 'nodes':{}, 'head':CacheNode..., 'tail':CacheNode...}; key=1; value=10

L5 __init__: self.key, self.value = key, value
self=CacheNode{'key':1, 'value':10}; key=1; value=10

... 33 intermediate state changes omitted ...

L24 get: if key not in self.nodes:
self=LRUCache{'capacity':2, 'nodes':{...:..., ...:...}, 'head':CacheNode..., 'tail':CacheNode...}; key=3

L26 get: node = self.nodes[key]
self=LRUCache{'capacity':2, 'nodes':{...:..., ...:...}, 'head':CacheNode..., 'tail':CacheNode...}; key=3; node=CacheNode{'key':3, 'value':30, 'prev':CacheNode..., 'next':CacheNode...}

L15 _detach: node.prev.next = node.next
self=LRUCache{'capacity':2, 'nodes':{...:..., ...:...}, 'head':CacheNode..., 'tail':CacheNode...}; node=CacheNode{'key':3, 'value':30, 'prev':CacheNode..., 'next':CacheNode...}

L19 _append_recent: previous = self.tail.prev
self=LRUCache{'capacity':2, 'nodes':{...:..., ...:...}, 'head':CacheNode..., 'tail':CacheNode...}; node=CacheNode{'key':3, 'value':30, 'prev':CacheNode..., 'next':CacheNode...}; previous=CacheNode{'key':1, 'value':10, 'prev':CacheNode..., 'next':CacheNode...}

Result: [None, None, 10, None, -1, 30]. Reading key 1 makes key 2 the eviction victim.

run_operations(LRUCache,[("put",1,10),("put",1,11),("get",1)],1)

L10 __init__: self.capacity, self.nodes = capacity, {}
self=LRUCache{'capacity':1, 'nodes':{}}; capacity=1

L5 __init__: self.key, self.value = key, value
self=CacheNode{'key':None, 'value':None}; key=None; value=None

L6 __init__: self.prev = self.next = None
self=CacheNode{'key':None, 'value':None, 'prev':None, 'next':None}; key=None; value=None

L5 __init__: self.key, self.value = key, value
self=CacheNode{'key':None, 'value':None}; key=None; value=None

L6 __init__: self.prev = self.next = None
self=CacheNode{'key':None, 'value':None, 'prev':None, 'next':None}; key=None; value=None

L11 __init__: self.head, self.tail = CacheNode(), CacheNode()
self=LRUCache{'capacity':1, 'nodes':{}, 'head':CacheNode..., 'tail':CacheNode...}; capacity=1

L32 put: if key in self.nodes:
self=LRUCache{'capacity':1, 'nodes':{}, 'head':CacheNode..., 'tail':CacheNode...}; key=1; value=10

L5 __init__: self.key, self.value = key, value
self=CacheNode{'key':1, 'value':10}; key=1; value=10

... 12 intermediate state changes omitted ...

L24 get: if key not in self.nodes:
self=LRUCache{'capacity':1, 'nodes':{...:...}, 'head':CacheNode..., 'tail':CacheNode...}; key=1

L26 get: node = self.nodes[key]
self=LRUCache{'capacity':1, 'nodes':{...:...}, 'head':CacheNode..., 'tail':CacheNode...}; key=1; node=CacheNode{'key':1, 'value':11, 'prev':CacheNode..., 'next':CacheNode...}

L15 _detach: node.prev.next = node.next
self=LRUCache{'capacity':1, 'nodes':{...:...}, 'head':CacheNode..., 'tail':CacheNode...}; node=CacheNode{'key':1, 'value':11, 'prev':CacheNode..., 'next':CacheNode...}

L19 _append_recent: previous = self.tail.prev
self=LRUCache{'capacity':1, 'nodes':{...:...}, 'head':CacheNode..., 'tail':CacheNode...}; node=CacheNode{'key':1, 'value':11, 'prev':CacheNode..., 'next':CacheNode...}; previous=CacheNode{'key':None, 'value':None, 'prev':None, 'next':CacheNode...}

Result: [None, None, 11]. Updating an existing key replaces its value without adding a duplicate node.

Why the solution is correct

Detaching and appending changes only recency, preserving all other entries and links. Every successful access or update becomes most recent by construction. New entries also enter there. Thus head.next is always the least recently used entry, and removing it implements exactly the required eviction policy.

Time and space

Expected O(1) time per get/put with hash lookup and constant pointer changes; O(capacity) stored entries. The sentinels use constant extra space.

Common mistakes

Updating a value without refreshing recency violates the access policy. Deleting only from the list or only from the map leaves inconsistent membership. Sentinel nodes must never be returned as cached data.

Reasoning card

State: nodes maps exactly cached keys to their list nodes. The list contains each cached node once, ordered from least to most recent, with mutually consistent prev/next links.

Invariant: Order runs from least recent to most recent and contains exactly cached keys.

Decision to rehearse: Combine hash lookup with recency ordering.

Finish: If capacity is exceeded, detach head.next and delete its map entry

Transfer exercise

Add expiration times. Identify why recency ordering alone cannot efficiently find the next expiring entry.