Implement LRU cache with O(1) ops
Company: Amazon
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: This interview question evaluates algorithm design, data structures, correctness, complexity, edge cases, and implementation details in a realistic interview setting. A strong answer for Implement LRU cache with O(1) ops states assumptions, handles edge cases, explains trade-offs, and shows how to validate the result clearly.
Constraints
- 1 <= capacity <= 10^4
- 0 <= number of operations <= 10^5
- Keys and values are integers in the range -10^9 .. 10^9
- Each 'put' has the form ["put", key, value]; each 'get' has the form ["get", key]
- get on a missing key returns -1 (values are otherwise valid integers, including negative ones)
Examples
Input: (2, [['put', 1, 1], ['put', 2, 2], ['get', 1], ['put', 3, 3], ['get', 2], ['put', 4, 4], ['get', 1], ['get', 3], ['get', 4]])
Expected Output: [None, None, 1, None, -1, None, -1, 3, 4]
Explanation: Capacity 2, the canonical LRU trace. put(1,1) and put(2,2) fill the cache. get(1)=1 makes 1 most recent (order now: 1 newest, 2 oldest). put(3,3) overflows and evicts the LRU key 2. get(2)=-1 (evicted). put(4,4) evicts the now-LRU key 1. get(1)=-1, get(3)=3, get(4)=4.
Input: (1, [['put', 2, 1], ['get', 2], ['put', 3, 2], ['get', 2], ['get', 3]])
Expected Output: [None, 1, None, -1, 2]
Explanation: Capacity 1 edge case. put(2,1) then get(2)=1. put(3,2) must evict key 2 because only one slot exists, so get(2)=-1 and get(3)=2.
Input: (2, [['get', 1]])
Expected Output: [-1]
Explanation: Reading from an empty cache returns -1 — the missing-key contract, with no puts performed first.
Input: (2, [['put', 1, 0], ['put', 2, 2], ['put', 1, 5], ['get', 1], ['get', 2]])
Expected Output: [None, None, None, 5, 2]
Explanation: Updating an existing key: put(1,0), put(2,2), then put(1,5) overwrites key 1's value to 5 and refreshes its recency without changing the size. No eviction occurs (still 2 entries), so get(1)=5 and get(2)=2.
Input: (3, [['put', 1, 1], ['put', 2, 2], ['put', 3, 3], ['get', 1], ['put', 4, 4], ['get', 2], ['get', 1], ['get', 3], ['get', 4]])
Expected Output: [None, None, None, 1, None, -1, 1, 3, 4]
Explanation: Capacity 3. After filling with 1,2,3, get(1) makes 1 most recent (LRU is now 2). put(4,4) overflows and evicts the LRU key 2, so get(2)=-1, while get(1)=1, get(3)=3, get(4)=4 all survive.
Input: (2, [['put', -1, -10], ['get', -1], ['put', -2, -20], ['put', -3, -30], ['get', -1], ['get', -2], ['get', -3]])
Expected Output: [None, -10, None, None, -1, -20, -30]
Explanation: Negative keys and values. get(-1)=-10 confirms -10 is a real stored value, distinct from the -1 missing-key sentinel. After put(-3,-30) overflows and evicts the LRU key -1, get(-1)=-1 (missing), get(-2)=-20, get(-3)=-30.
Hints
- You need two things at once: O(1) lookup by key, and O(1) knowledge of which key is least recently used. A hash map alone gives you the first but not the second.
- Pair a hash map (key -> node) with a doubly linked list ordered by recency: head = most recently used, tail = least recently used. On any access, splice the node to the head; to evict, remove the tail.
- Treat an update to an existing key like an access: refresh its recency, then overwrite its value. Eviction should only trigger after an insertion makes the size exceed capacity — never on a get or on an in-place update.
- In Python, collections.OrderedDict does all of this for you: move_to_end(key) refreshes recency and popitem(last=False) removes the LRU entry. In Java, a LinkedHashMap with accessOrder=true plus an overridden removeEldestEntry is the equivalent shortcut.