Implement an LRU cache
Company: Affirm
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: easy
Interview Round: Onsite
Quick Answer: This question evaluates a candidate's ability to design and implement efficient cache eviction policies while managing time and space complexity guarantees for mutable key-value stores.
Constraints
- 1 <= capacity <= 10^5
- 0 <= len(operations) <= 2 * 10^5
- operations[i] is either [0, key] or [1, key, value]
- -10^9 <= key, value <= 10^9
- Both get and put must run in average O(1) time
Examples
Input: (2, [[1,1,1], [1,2,2], [0,1], [1,3,3], [0,2], [1,4,4], [0,1], [0,3], [0,4]])
Expected Output: [1, -1, -1, 3, 4]
Explanation: After get(1), key 1 becomes most recent. put(3,3) evicts key 2. Later put(4,4) evicts key 1. The get results are 1, -1, -1, 3, and 4.
Input: (2, [[1,1,1], [1,2,2], [1,1,10], [0,1], [1,3,3], [0,2], [0,1], [0,3]])
Expected Output: [10, -1, 10, 3]
Explanation: Updating key 1 changes its value to 10 and makes it most recent. When key 3 is inserted, key 2 is least recently used and is evicted.
Input: (1, [[1,5,5], [0,5], [1,6,6], [0,5], [0,6], [1,6,60], [0,6]])
Expected Output: [5, -1, 6, 60]
Explanation: With capacity 1, inserting key 6 evicts key 5. Updating key 6 changes its value from 6 to 60.
Input: (3, [])
Expected Output: []
Explanation: There are no operations, so there are no get results.
Input: (3, [[1,1,1], [1,2,2], [1,3,3], [0,1], [0,2], [1,4,4], [0,3], [0,1], [0,2], [0,4]])
Expected Output: [1, 2, -1, 1, 2, 4]
Explanation: get(1) and get(2) update recency, making key 3 the least recently used. Therefore put(4,4) evicts key 3.
Input: (2, [[1,-1,-10], [1,2,20], [0,-1], [1,3,30], [0,2], [0,-1], [0,3]])
Expected Output: [-10, -1, -10, 30]
Explanation: Negative keys and values are supported. Accessing key -1 makes it recent, so inserting key 3 evicts key 2.
Solution
def solution(capacity, operations):
class Node:
__slots__ = ('key', 'value', 'prev', 'next')
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
if capacity <= 0:
return [-1 for op in operations if op[0] == 0]
cache = {}
# Dummy sentinels: head.next is most recently used, tail.prev is least recently used.
head = Node()
tail = Node()
head.next = tail
tail.prev = head
def remove(node):
before = node.prev
after = node.next
before.next = after
after.prev = before
def insert_at_front(node):
first = head.next
node.prev = head
node.next = first
head.next = node
first.prev = node
def move_to_front(node):
remove(node)
insert_at_front(node)
def evict_lru():
lru = tail.prev
if lru is head:
return
remove(lru)
del cache[lru.key]
result = []
for op in operations:
if op[0] == 0:
key = op[1]
node = cache.get(key)
if node is None:
result.append(-1)
else:
move_to_front(node)
result.append(node.value)
else:
key = op[1]
value = op[2]
node = cache.get(key)
if node is not None:
node.value = value
move_to_front(node)
else:
if len(cache) >= capacity:
evict_lru()
new_node = Node(key, value)
cache[key] = new_node
insert_at_front(new_node)
return resultTime complexity: O(n), where n is the number of operations. Each get and put runs in average O(1) time.. Space complexity: O(capacity).
Hints
- Use a hash map to find cache entries by key in O(1) average time.
- Use a doubly linked list to track recency: move accessed or updated nodes to the front, and evict from the back.