Fixed-Capacity Key-Value Cache That Evicts the Least Recently Used Key
Company: Alibaba
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Implement a key-value cache with a fixed capacity. When the cache is full and a new key has to be inserted, the cache evicts the key that was used least recently.
The cache is tested through one function: it receives the capacity and a sequence of operations, applies them in order to an initially empty cache, and returns the results of the read operations.
### Function Signature
```python
def simulate_lru_cache(capacity: int, operations: list[str], arguments: list[list[int]]) -> list[int]:
```
`operations[i]` is `"get"` or `"put"`. For `"get"`, `arguments[i]` is `[key]`; for `"put"`, it is `[key, value]`.
### Rules
- `get(key)`: if `key` is in the cache, the result is its value, and `key` becomes the most recently used key. Otherwise the result is `-1` and the cache does not change.
- `put(key, value)`: if `key` is already in the cache, its value is replaced and it becomes the most recently used key; nothing is evicted. Otherwise, if the cache already holds `capacity` keys, the least recently used key is removed first; then `key` is inserted with `value` and becomes the most recently used key.
- A key is used by every `put` on it and by every `get` that finds it. A `get` that misses does not count as a use.
- Return one integer per `"get"` operation, in the order the operations appear. `"put"` operations produce no output.
- Each operation must run in $O(1)$ average time.
### Constraints
- `1 <= capacity <= 10^4`
- `1 <= len(operations) <= 2 * 10^5`, and `len(arguments) == len(operations)`
- `0 <= key <= 10^5`
- `0 <= value <= 10^9`
- Every operation is `"get"` or `"put"`, with the matching number of arguments.
### Examples
**Example 1**
```text
Input: capacity = 2,
operations = ["put", "put", "get", "put", "get", "get", "put", "get", "put", "get", "get"],
arguments = [[1, 10], [2, 20], [1], [3, 30], [2], [3], [1, 15], [1], [4, 40], [3], [1]]
Output: [10, -1, 30, 15, -1, 15]
```
After the first two operations the cache holds keys 1 and 2, with key 1 the least recently used. `get(1)` returns 10 and makes key 2 the least recently used, so `put(3, 30)` evicts key 2 and `get(2)` returns -1. `get(3)` returns 30. `put(1, 15)` replaces key 1's value without evicting anything and makes key 3 the least recently used; `get(1)` returns 15. `put(4, 40)` evicts key 3, so `get(3)` returns -1, and `get(1)` returns 15.
**Example 2**
```text
Input: capacity = 1,
operations = ["put", "put", "get", "put", "get", "get"],
arguments = [[5, 1], [5, 2], [5], [6, 3], [5], [6]]
Output: [2, -1, 3]
```
The second `put` on key 5 replaces its value and evicts nothing. With room for one key, `put(6, 3)` evicts key 5.
**Example 3**
```text
Input: capacity = 2,
operations = ["get", "put", "put", "get", "get", "put", "get", "get", "get"],
arguments = [[7], [7, 0], [8, 1], [7], [9], [9, 2], [8], [7], [9]]
Output: [-1, 0, -1, -1, 0, 2]
```
Reading key 7 makes key 8 the least recently used. The miss on key 9 changes nothing, so `put(9, 2)` evicts key 8. A stored value of 0 is returned as 0.
Overview: Implement a fixed-capacity key-value cache that evicts the least recently used key when it is full, driven through a single function that replays get and put operations. It tests recency bookkeeping, update-versus-insert semantics and keeping every operation at constant average time.
Read the full Alibaba Software Engineer interview experience this question came from
Implement a key-value cache with a fixed capacity. When the cache is full and a new key has to be inserted, the cache evicts the key that was used least recently.
The cache is tested through one function, `simulate_lru_cache(capacity, operations, arguments)`: it starts from an empty cache, applies the operations in order, and returns the results of the read operations.
`operations[i]` is `"get"` or `"put"`. For `"get"`, `arguments[i]` is `[key]`; for `"put"`, `arguments[i]` is `[key, value]`. (In JavaScript the third parameter is named `args`, because `arguments` is a special name inside JavaScript functions.)
### Rules
- `get(key)`: if `key` is in the cache, the result is its value and `key` becomes the most recently used key. Otherwise the result is `-1` and the cache does not change.
- `put(key, value)`: if `key` is already in the cache, its value is replaced and it becomes the most recently used key; nothing is evicted. Otherwise, if the cache already holds `capacity` keys, the least recently used key is removed first; then `key` is inserted with `value` and becomes the most recently used key.
- A key is used by every `put` on it and by every `get` that finds it. A `get` that misses does not count as a use.
- Return one integer per `"get"` operation, in the order the operations appear. `"put"` operations produce no output, so a sequence with no `"get"` returns an empty list.
- Each operation must run in O(1) average time.
Every key, value and result fits in a signed 32-bit integer (values are at most 10^9, and a miss is reported as -1).
### Constraints
- `1 <= capacity <= 10^4`
- `1 <= len(operations) <= 2 * 10^5`, and `len(arguments) == len(operations)`
- `0 <= key <= 10^5`
- `0 <= value <= 10^9`
- Every operation is `"get"` or `"put"`, with the matching number of arguments.
### Example 1
```text
Input: capacity = 2,
operations = ["put", "put", "get", "put", "get", "get", "put", "get", "put", "get", "get"],
arguments = [[1, 10], [2, 20], [1], [3, 30], [2], [3], [1, 15], [1], [4, 40], [3], [1]]
Output: [10, -1, 30, 15, -1, 15]
```
After the first two operations the cache holds keys 1 and 2, with key 1 the least recently used. `get(1)` returns 10 and makes key 2 the least recently used, so `put(3, 30)` evicts key 2 and `get(2)` returns -1. `get(3)` returns 30. `put(1, 15)` replaces key 1's value without evicting anything and makes key 3 the least recently used; `get(1)` returns 15. `put(4, 40)` evicts key 3, so `get(3)` returns -1, and `get(1)` returns 15.
### Example 2
```text
Input: capacity = 2,
operations = ["get", "put", "put", "get", "get", "put", "get", "get", "get"],
arguments = [[7], [7, 0], [8, 1], [7], [9], [9, 2], [8], [7], [9]]
Output: [-1, 0, -1, -1, 0, 2]
```
Reading key 7 makes key 8 the least recently used. The miss on key 9 changes nothing, so `put(9, 2)` evicts key 8. A stored value of 0 is returned as 0, which is different from the -1 reported for a miss.
Constraints
- 1 <= capacity <= 10^4
- 1 <= len(operations) <= 2 * 10^5, and len(arguments) == len(operations)
- 0 <= key <= 10^5
- 0 <= value <= 10^9
- Every operations[i] is "get" or "put"; arguments[i] is [key] for "get" and [key, value] for "put"
- The cache starts empty; every key, value and result fits in a signed 32-bit integer
Examples
Input: (2, ['put', 'put', 'get', 'put', 'get', 'get', 'put', 'get', 'put', 'get', 'get'], [[1, 10], [2, 20], [1], [3, 30], [2], [3], [1, 15], [1], [4, 40], [3], [1]])
Expected Output: [10, -1, 30, 15, -1, 15]
Explanation: Source Example 1: the get hit refreshes key 1 so put 3 evicts key 2; put on existing key 1 replaces its value without eviction; put 4 evicts key 3.
Input: (1, ['put', 'put', 'get', 'put', 'get', 'get'], [[5, 1], [5, 2], [5], [6, 3], [5], [6]])
Expected Output: [2, -1, 3]
Explanation: Source Example 2: capacity 1; the second put on key 5 only replaces the value; put 6 evicts key 5.
Hints
- For every stored key you need its value and how recently it was last used. A get that misses is not a use and changes nothing.
- A put on a key that is already stored only updates and refreshes it, so decide whether the key is present before deciding whether anything must be evicted.
- The O(1) average bound means you can neither search the cache for a key nor scan it to find the least recently used key.