Fatskills
Practice. Master. Repeat.
Study Guide: How to Solve: LRU Cache (Design) – Complete Guide for FAANG Interviews
Source: https://www.fatskills.com/leetcode/chapter/how-to-solve-lru-cache-design-complete-guide-for-faang-interviews

How to Solve: LRU Cache (Design) – Complete Guide for FAANG Interviews

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~7 min read

How to Solve: LRU Cache (Design) – Complete Guide for FAANG Interviews


? Introduction

"This problem appears in 1 out of every 3 onsite interviews—nail it, and you prove you can design real-world systems with optimal trade-offs between speed and memory."


? WHAT YOU NEED TO KNOW FIRST

Before tackling LRU Cache, ensure you understand: 1. Hash Maps (Dictionaries) – O(1) average-time lookups/insertions/deletions. 2. Doubly Linked Lists – O(1) insertions/deletions at both ends. 3. Time-Space Trade-offs – Balancing fast access with memory constraints.


? KEY TECHNIQUES & PATTERNS

Technique / Pattern When to Use Quick Example
Hash Map + Doubly Linked List O(1) get/put operations with LRU eviction cache = {key: Node}, head/tail pointers
Dummy Head/Tail Nodes Simplify edge cases (empty list) head.next = tail, tail.prev = head
Node Removal/Insertion Maintain O(1) operations remove(node), add_to_front(node)
Capacity Check Evict LRU item when full if len(cache) > capacity: remove(tail.prev)
Order Maintenance Track usage order Move accessed nodes to front (get or put)
Edge Case Handling Empty cache, single item, duplicates Check if key in cache before operations

? STEP-BY-STEP FRAMEWORK (Mental Checklist)

Follow these steps every time you solve an LRU Cache problem:

  1. Clarify Requirements
  2. Confirm get(key) returns -1 if key doesn’t exist.
  3. Confirm put(key, value) evicts LRU item if capacity is exceeded.

  4. Choose Data Structures

  5. Hash Map: Store key → Node for O(1) access.
  6. Doubly Linked List: Maintain usage order (most recent at head, LRU at tail).

  7. Design Node Structure

  8. Each node has key, value, prev, next.

  9. Initialize Dummy Head/Tail

  10. Simplifies edge cases (e.g., empty list).

  11. Implement Helper Methods

  12. _remove(node): Detach node from list.
  13. _add_to_front(node): Insert node after head.

  14. Implement get(key)

  15. If key exists: Move node to front, return value.
  16. Else: Return -1.

  17. Implement put(key, value)

  18. If key exists: Update value, move to front.
  19. Else:

    • If cache is full: Evict tail.prev (LRU).
    • Add new node to front.
  20. Test Edge Cases

  21. Empty cache, single item, duplicates, full capacity.

? WORKED EXAMPLES

Example 1 – Basic LRU Cache (LeetCode 146)

Problem: Design an LRU cache with get and put in O(1) time.

Thought Process

  1. Use a hash map (dict) for O(1) lookups.
  2. Use a doubly linked list to track usage order.
  3. Dummy head/tail nodes simplify edge cases.

Python Code

class Node:

def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {}
self.head = Node() # Dummy head
self.tail = Node() # Dummy tail
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def _add_to_front(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node = self.cache[key]
self._remove(node)
self._add_to_front(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._remove(node)
self._add_to_front(node)
else:
if len(self.cache) >= self.capacity:
lru_node = self.tail.prev
self._remove(lru_node)
del self.cache[lru_node.key]
new_node = Node(key, value)
self.cache[key] = new_node
self._add_to_front(new_node)

Complexity

  • Time: O(1) for get and put.
  • Space: O(capacity) for hash map and linked list.

Why This Works

  • Hash map provides O(1) access to nodes.
  • Doubly linked list maintains usage order in O(1) time.

Example 2 – Medium Twist: LRU Cache with Expiration (Follow-up)

Problem: Extend LRU Cache to support get(key, timestamp) where items expire after ttl seconds.

Thought Process

  1. Add expiry_time to each node.
  2. In get, check if timestamp > expiry_time → evict and return -1.
  3. In put, set expiry_time = timestamp + ttl.

Python Code

class Node:

def __init__(self, key=0, value=0, expiry_time=0):
self.key = key
self.value = value
self.expiry_time = expiry_time
self.prev = None
self.next = None class LRUCache:
def __init__(self, capacity: int, ttl: int):
self.capacity = capacity
self.ttl = ttl
self.cache = {}
self.head = Node()
self.tail = Node()
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def _add_to_front(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key: int, timestamp: int) -> int:
if key not in self.cache:
return -1
node = self.cache[key]
if timestamp > node.expiry_time:
self._remove(node)
del self.cache[key]
return -1
self._remove(node)
self._add_to_front(node)
return node.value
def put(self, key: int, value: int, timestamp: int) -> None:
if key in self.cache:
node = self.cache[key]
node.value = value
node.expiry_time = timestamp + self.ttl
self._remove(node)
self._add_to_front(node)
else:
if len(self.cache) >= self.capacity:
lru_node = self.tail.prev
self._remove(lru_node)
del self.cache[lru_node.key]
new_node = Node(key, value, timestamp + self.ttl)
self.cache[key] = new_node
self._add_to_front(new_node)

Why This Works

  • Extends the basic LRU with expiry checks.
  • Still maintains O(1) operations.

Example 3 – Hard Follow-up: LRU Cache with Frequency Tracking

Problem: Design an LRU cache that also tracks access frequency (e.g., for analytics).

Thought Process

  1. Add frequency to each node.
  2. In get, increment frequency and move to front.
  3. In put, update frequency if key exists.

Python Code

class Node:

def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.frequency = 1
self.prev = None
self.next = None class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {}
self.head = Node()
self.tail = Node()
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def _add_to_front(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node = self.cache[key]
node.frequency += 1
self._remove(node)
self._add_to_front(node)
return node.value
def put(self, key: int, value: int) -> None:
if key in self.cache:
node = self.cache[key]
node.value = value
node.frequency += 1
self._remove(node)
self._add_to_front(node)
else:
if len(self.cache) >= self.capacity:
lru_node = self.tail.prev
self._remove(lru_node)
del self.cache[lru_node.key]
new_node = Node(key, value)
self.cache[key] = new_node
self._add_to_front(new_node)

Why This Works

  • Extends LRU with frequency tracking without sacrificing O(1) operations.

❌ Common Mistakes

Mistake Why It Happens Correct Approach
Using a Singly Linked List Forgetting O(1) removal requires prev Use doubly linked list for O(1) removal.
Not Updating Order on get Assuming get doesn’t affect LRU order Always move accessed nodes to front.
Forgetting Capacity Check Evicting only on put without checking size Check len(cache) >= capacity before inserting.
Not Handling Dummy Nodes Edge cases (empty list) cause crashes Use dummy head/tail nodes.
Storing Value Instead of Node Hash map maps key → value instead of key → Node Store key → Node to enable O(1) removal.

? INTERVIEW TrapS

Trap How to Spot It How to Avoid It
"What if the cache is empty?" Interviewer asks about edge cases. Test with capacity=0 or empty cache.
"Can you do it with O(1) space?" Follow-up question after initial solution. Clarify: O(1) space is impossible (need O(capacity) for hash map + linked list).
"How would you test this?" Interviewer asks for test cases. List edge cases: empty cache, single item, duplicates, full capacity, expiry.

? 1-Minute Recap

"Alright, let’s lock this in. LRU Cache is all about O(1) operations—so you must combine a hash map with a doubly linked list. Here’s the playbook:

  1. Hash Map gives you O(1) lookups.
  2. Doubly Linked List keeps usage order in O(1) time.
  3. Dummy Head/Tail simplify edge cases.
  4. Move to Front on every get or put.
  5. Evict LRU when capacity is exceeded.

Test edge cases: empty cache, single item, duplicates, full capacity. If they ask for a twist—like expiry or frequency—extend the node structure, but keep the core O(1) operations.

You’ve got this. Now go crush that interview!


? Final Notes

  • Practice: Implement LRU Cache 3 times from scratch.
  • Whiteboard: Draw the hash map + linked list structure.
  • Follow-ups: Be ready for expiry, frequency, or multi-threaded variants.

This framework is interview-ready—use it verbatim. Good luck! ?



ADVERTISEMENT