Quick 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.

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

  1. 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.
  2. 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.
  3. 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.

Loading coding console...

Show the approach

Approach

Keep the cached keys in a doubly linked list ordered by recency, between a head sentinel (most recently used side) and a tail sentinel (least recently used side). Because every key lies in 0..10^5, the list links, the stored value and an in-cache flag are kept in arrays indexed directly by key (a hash map from key to list node is equivalent), so locating a key and relinking it both take O(1).

  • get(key): on a hit, unlink the key, relink it right after the head, and report its value; on a miss, report -1 and change nothing.
  • put(key, value): if the key is cached, overwrite its value and move it right after the head; nothing is evicted and the size is unchanged. Otherwise, if the cache already holds capacity keys, unlink the node right before the tail (the least recently used key) and clear its flag; then store the value, set the flag and link the key right after the head.

Invariant: after every operation the list contains exactly the cached keys, at most capacity of them, ordered by the time of their last use (a put, or a get that found them), most recent first. Each rule of the statement is one list update that preserves this invariant, so every reported value and every eviction matches the specification.

Edge cases: gets on an empty cache return -1; capacity 1 evicts on every put of a different key; repeated puts on a cached key never evict and never grow the size; a stored value of 0 is returned as 0, distinct from the -1 miss; a sequence without gets returns an empty list. All quantities fit in 32-bit integers.

Time complexity:
O(n + K), where n = len(operations) and K = 10^5 + 1 key slots initialized once; each operation is O(1)
Space complexity:
O(n + K): the key-indexed arrays plus the output list