Skip to content

LRU Cache

Design a data structure that follows the constraints of a Least Recently Used (LRU) Cache.

Implement the LRUCache class:

  • LRUCache(int capacity) — Initialize the LRU cache with positive size capacity.
  • int get(int key) — Return the value of the key if the key exists, otherwise return -1.
  • void put(int key, int value) — Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity, evict the least recently used key.

Both get() and put() must run in O(1) average time complexity.

OperationCapacityState BeforeState AfterReturn
LRUCache(2)2{}{}
put(1, 1)2{}{1: 1}
put(2, 2)2{1: 1}{1: 1, 2: 2}
get(1)2{1: 1, 2: 2} (1 is recent){1: 1, 2: 2}1
put(3, 3)2{1: 1, 2: 2}{1: 1, 3: 3} (2 evicted)
get(2)2{1: 1, 3: 3}{1: 1, 3: 3}-1
put(4, 4)2{1: 1, 3: 3}{3: 3, 4: 4} (1 evicted)
get(1)2{3: 3, 4: 4}{3: 3, 4: 4}-1
get(3)2{3: 3, 4: 4} (3 is recent){3: 3, 4: 4}3
get(4)2{3: 3, 4: 4} (4 is recent){3: 3, 4: 4}4
  • 1 <= capacity <= 3000
  • 0 <= key <= 10^4
  • 0 <= value <= 10^5
  • At most 3 × 10^4 calls to get() and put().
ApproachTime (get/put)SpaceEvictionBest When
Hash Map + Doubly Linked ListO(1) / O(1)O(capacity)Constant-time node removalGeneral case — full control, fast in all scenarios
OrderedDict / LinkedHashMapO(1) / O(1)O(capacity)Constant-time via language featureYou trust built-in ordered structures, cleaner code
HashMap + Priority QueueO(log n) / O(log n)O(capacity)Log-time priority updatesNot recommended; slower than the above
  • Pick Hash Map + Doubly Linked List if you want to understand the mechanics and impress in interviews.
  • Pick OrderedDict if you are practicing a language’s built-in data structures and want cleaner code.
Best For Interviews
Hash Map + Doubly Linked List
O(1) time · O(capacity) space
Cleanest Code
OrderedDict / LinkedHashMap
O(1) time · O(capacity) space
Section titled “Approach 1: Hash Map + Doubly Linked List (Recommended)”
★ Recommended

Combine a hash map for O(1) lookups with a doubly linked list to track access order. The most recently used item is kept at the head, and the least recently used item is at the tail (ready to evict).

The key insight is that moving a node to the head and removing a node from the tail are both O(1) operations when you have pointers.

⏱ Time O(1) Hash lookup + list pointer ops 💾 Space O(capacity) Hash + linked list nodes

Sequence: LRUCache(2)put(1,1)put(2,2)get(1)put(3,3)get(2)

StepOperationCache StateDLL Order (head→tail)Action
1put(1, 1){1: 1}1Insert 1 at head
2put(2, 2){1: 1, 2: 2}2 → 1Insert 2 at head
3get(1){1: 1, 2: 2}1 → 2Move 1 to head (accessed)
4put(3, 3){1: 1, 3: 3}3 → 1Insert 3, evict 2 (least recent)
5get(2){1: 1, 3: 3}3 → 1Return -1 (2 not in cache)

Animated walkthrough

How the LRU cache evicts the least recently used item

Watch the doubly linked list reorder as items are accessed. The least recently used item (at the tail) is evicted when the cache exceeds capacity.

Step 1 of 5 Target: capacity
Waiting to begin...
Array state
Memory / lookup state

class DLLNode:
key, value, prev, next
class LRUCache:
capacity, cache (HashMap), head, tail (dummy nodes)
get(key):
if key not in cache:
return -1
node = cache[key]
moveToHead(node) // Mark as recently used
return node.value
put(key, value):
if key in cache:
node = cache[key]
node.value = value
moveToHead(node)
else:
if cache.size >= capacity:
removeTail() // Evict least recently used
node = new DLLNode(key, value)
cache[key] = node
addToHead(node)
moveToHead(node):
removeNode(node)
addToHead(node)
removeNode(node):
node.prev.next = node.next
node.next.prev = node.prev
addToHead(node):
node.next = head.next
node.prev = head
head.next.prev = node
head.next = node
removeTail():
node = tail.prev
removeNode(node)
cache.remove(node.key)
lru_cache_hash_dll.py
from typing import Optional
class DLLNode:
def __init__(self, key: int = 0, value: int = 0):
self.key = key
self.value = value
self.prev: Optional['DLLNode'] = None
self.next: Optional['DLLNode'] = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {} # key -> node
self.head = DLLNode() # dummy head
self.tail = DLLNode() # dummy tail
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node = self.cache[key]
self._move_to_head(node)
return node.value
def put(self, key: int, value: int) -> None:
if key in self.cache:
node = self.cache[key]
node.value = value
self._move_to_head(node)
else:
if len(self.cache) >= self.capacity:
self._remove_tail()
node = DLLNode(key, value)
self.cache[key] = node
self._add_to_head(node)
def _move_to_head(self, node: DLLNode) -> None:
self._remove_node(node)
self._add_to_head(node)
def _remove_node(self, node: DLLNode) -> None:
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_head(self, node: DLLNode) -> None:
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def _remove_tail(self) -> None:
node = self.tail.prev
self._remove_node(node)
del self.cache[node.key]
# Test
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1)) # 1
cache.put(3, 3)
print(cache.get(2)) # -1
cache.put(4, 4)
print(cache.get(1)) # -1
print(cache.get(3)) # 3
print(cache.get(4)) # 4
🎯 Interview Favourite

Use a language’s built-in ordered hash map (Python’s OrderedDict, Java’s LinkedHashMap, JavaScript’s Map) which maintains insertion order and allows fast key reordering.

This approach achieves the same O(1) time complexity with less code, since the language handles the doubly linked list for you.

⏱ Time O(1) Built-in ordered ops 💾 Space O(capacity) Hash + internal linked list

Same sequence as Approach 1: LRUCache(2)put(1,1)put(2,2)get(1)put(3,3)get(2)

With OrderedDict, the order is maintained automatically:

StepOperationOrderedDict StateAction
1put(1, 1){1: 1}Insert; order is [1]
2put(2, 2){1: 1, 2: 2}Insert; order is [1, 2]
3get(1){1: 1, 2: 2}Access 1; move to end: order is [2, 1]
4put(3, 3){1: 1, 3: 3}Insert 3; pop oldest (2); order is [1, 3]
5get(2){1: 1, 3: 3}Return -1 (2 not found)
class LRUCache:
capacity, cache (OrderedDict)
get(key):
if key not in cache:
return -1
cache.move_to_end(key) // Language handles reordering
return cache[key]
put(key, value):
if key in cache:
cache[key] = value
cache.move_to_end(key)
else:
if cache.size >= capacity:
cache.popitem(FIFO) // Remove oldest (first inserted)
cache[key] = value
lru_cache_ordered_dict.py
from collections import OrderedDict
from typing import Any
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache: OrderedDict[int, int] = OrderedDict()
def get(self, key: int) -> int:
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache[key] = value
self.cache.move_to_end(key)
else:
if len(self.cache) >= self.capacity:
self.cache.popitem(last=False)
self.cache[key] = value
# Test
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1)) # 1
cache.put(3, 3)
print(cache.get(2)) # -1
cache.put(4, 4)
print(cache.get(1)) # -1
print(cache.get(3)) # 3
print(cache.get(4)) # 4
✓ Simple

A third approach demonstrating an alternative technique for solving this problem. This implementation optimizes either time or space complexity compared to the previous approaches.

⏱ Time O(n) Optimized complexity 💾 Space O(1) Efficient space usage
function approach3():
// Implementation approach goes here
lru_cache_space_optimized.py
"""
Solution for LRU Cache
"""
def solve():
"""Implementation for approach 3 of LRU Cache"""
pass
if __name__ == "__main__":
print("Solution ready")