Implement Cache and Key-Value Store
Company: Lyft
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates the ability to design efficient in-memory data structures and caching mechanisms, focusing on core key-value operations, eviction behavior, and performance constraints.
Part 1: Design an LRU Bounded Cache
Constraints
- 0 <= capacity <= 100000
- 0 <= len(operations) <= 200000
- Each operation is either ('put', key, value) or ('get', key)
- -10^9 <= key, value <= 10^9
Examples
Input: (2, [('put', 1, 10), ('put', 2, 20), ('get', 1), ('put', 3, 30), ('get', 2), ('put', 1, 15), ('get', 1), ('get', 3)])
Expected Output: [10, -1, 15, 30]
Explanation: After get(1), key 1 becomes most recent, so putting key 3 evicts key 2. Updating key 1 changes its value to 15 and keeps it recent.
Input: (0, [('put', 1, 1), ('get', 1), ('put', 2, 2), ('get', 2)])
Expected Output: [-1, -1]
Explanation: Edge case: capacity is 0, so the cache can never store anything.
Input: (1, [('put', 5, 50), ('put', 5, 55), ('get', 5), ('put', 6, 60), ('get', 5), ('get', 6)])
Expected Output: [55, -1, 60]
Explanation: Updating an existing key should not evict it. When key 6 is inserted into a full cache of size 1, key 5 is evicted.
Input: (3, [])
Expected Output: []
Explanation: Edge case: no operations means no output.
Hints
- A hash map can give O(1) access to cache entries by key, but you also need O(1) updates to recency order.
- A doubly linked list with dummy head and tail nodes is a common way to move items to the front and remove the least recently used item in O(1).
Part 2: Build an In-Memory Key-Value Store
Constraints
- 0 <= len(operations) <= 200000
- Each key is a non-empty string with length <= 100
- -10^9 <= value <= 10^9
- Each operation is one of ('put', key, value), ('get', key), or ('delete', key)
Examples
Input: ([('put', 'a', 1), ('put', 'b', 2), ('get', 'a'), ('put', 'a', 3), ('get', 'a'), ('delete', 'b'), ('get', 'b'), ('delete', 'x')],)
Expected Output: [1, 3, True, None, False]
Explanation: Basic insert, update, lookup, successful delete, missing lookup, and failed delete.
Input: ([],)
Expected Output: []
Explanation: Edge case: no operations.
Input: ([('delete', 'ghost'), ('put', 'ghost', 7), ('get', 'ghost'), ('delete', 'ghost'), ('get', 'ghost'), ('delete', 'ghost')],)
Expected Output: [False, 7, True, None, False]
Explanation: Deleting a missing key returns False. After deletion, later get returns None.
Input: ([('put', 'k0', 0), ('put', 'k1', 1), ('put', 'k2', 2), ('put', 'k3', 3), ('put', 'k4', 4), ('put', 'k5', 5), ('put', 'k6', 6), ('get', 'k6'), ('get', 'k0'), ('delete', 'k3'), ('get', 'k3')],)
Expected Output: [6, 0, True, None]
Explanation: Several inserts force the table to handle more entries and still return correct values after updates and deletion.
Hints
- Use an array of buckets, where each bucket stores a small list of (key, value) pairs.
- If too many items are stored relative to the number of buckets, resize and rehash all existing entries to keep average operations fast.