Quick Overview

Build a fixed-capacity key-value cache that evicts the key with the fewest uses, breaking ties by least recent use, and replay a sequence of get and put operations to return each read result. It tests careful per-key frequency and recency tracking with constant average time per operation.

Fixed-Capacity Cache That Evicts the Least Frequently Used Key, Ties by Recency

Company: Alibaba

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

Implement a key-value cache with a fixed capacity that, when it is full and a new key has to be inserted, evicts the key that has been used the fewest times. If several keys share the smallest use count, it evicts the one among them 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_lfu_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 - Every key in the cache has a use count. - `get(key)`: if `key` is in the cache, the result is its value, its use count increases by 1, and it 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, its use count increases by 1, and it becomes the most recently used key; nothing is evicted. Otherwise, if the cache already holds `capacity` keys, one key is evicted first: the key with the smallest use count, and among keys tied for the smallest count, the least recently used one. Then `key` is inserted with `value` and a use count of 1, and it becomes the most recently used key. - An evicted key loses its count. If it is inserted again later, its count starts again at 1. - 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 = 3, operations = ["put", "put", "put", "get", "get", "put", "get", "get", "put", "get", "put", "put", "get", "get", "get"], arguments = [[1, 100], [2, 200], [3, 300], [2], [3], [4, 400], [1], [4], [5, 500], [2], [3, 333], [6, 600], [5], [3], [6]] Output: [200, 300, -1, 400, -1, -1, 333, 600] ``` Key 1 is the only key never read, so `put(4, 400)` evicts it. Just before `put(5, 500)`, keys 2, 3 and 4 all have a use count of 2, and key 2 was used least recently, so it is evicted. `put(3, 333)` updates key 3 and raises its count to 3. Just before `put(6, 600)`, key 5 has the smallest count, 1, and is evicted. **Example 2** ```text Input: capacity = 1, operations = ["put", "get", "put", "get", "get"], arguments = [[1, 1], [1], [2, 2], [1], [2]] Output: [1, -1, 2] ``` With room for one key, `put(2, 2)` evicts key 1 even though key 1 has the higher use count, because it is the only key in the cache. **Example 3** ```text Input: capacity = 2, operations = ["put", "get", "put", "get", "get", "get", "put", "put", "put", "get", "get", "get"], arguments = [[1, 1], [1], [2, 2], [2], [2], [1], [3, 3], [2, 20], [4, 4], [1], [2], [4]] Output: [1, 2, 2, 1, 1, -1, 4] ``` Keys 1 and 2 both reach a use count of 3, and key 2 was used less recently, so `put(3, 3)` evicts key 2. `put(2, 20)` evicts key 3, whose count is 1, and key 2 comes back with a count of 1 rather than its old count, so `put(4, 4)` evicts key 2 instead of key 1.

Overview: Build a fixed-capacity key-value cache that evicts the key with the fewest uses, breaking ties by least recent use, and replay a sequence of get and put operations to return each read result. It tests careful per-key frequency and recency tracking with constant average time per operation.

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, it evicts the key that has been used the fewest times. If several keys share the smallest use count, it evicts the one among them that was used least recently. The cache is tested through one function, `simulate_lfu_cache(capacity, operations, arguments)`. It applies the operations in order to an initially empty cache and returns the results of the read operations. `operations[i]` is `"get"` or `"put"`. For `"get"`, `arguments[i]` is `[key]`; for `"put"`, it is `[key, value]`. ### Rules - Every key in the cache has a use count. - `get(key)`: if `key` is in the cache, the result is its value, its use count increases by 1, and it 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, its use count increases by 1, and it becomes the most recently used key; nothing is evicted. Otherwise, if the cache already holds `capacity` keys, one key is evicted first: the key with the smallest use count, and among keys tied for the smallest count, the least recently used one. Then `key` is inserted with `value` and a use count of 1, and it becomes the most recently used key. - An evicted key loses its count. If it is inserted again later, its count starts again at 1. - Return one integer per `"get"` operation, in the order the operations appear. `"put"` operations produce no output, so a sequence without any `"get"` returns an empty list. - 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. Every key, value and result fits in a signed 32-bit integer (none exceeds 2^31 - 1), so `int` is sufficient in Java and C++. ### Example 1 ```text Input: capacity = 3, operations = ["put", "put", "put", "get", "get", "put", "get", "get", "put", "get", "put", "put", "get", "get", "get"], arguments = [[1, 100], [2, 200], [3, 300], [2], [3], [4, 400], [1], [4], [5, 500], [2], [3, 333], [6, 600], [5], [3], [6]] Output: [200, 300, -1, 400, -1, -1, 333, 600] ``` Key 1 is the only key never read, so `put(4, 400)` evicts it. Just before `put(5, 500)`, keys 2, 3 and 4 all have a use count of 2, and key 2 was used least recently, so it is evicted. `put(3, 333)` updates key 3 and raises its count to 3. Just before `put(6, 600)`, key 5 has the smallest count, 1, and is evicted. ### Example 2 ```text Input: capacity = 2, operations = ["put", "get", "put", "get", "get", "get", "put", "put", "put", "get", "get", "get"], arguments = [[1, 1], [1], [2, 2], [2], [2], [1], [3, 3], [2, 20], [4, 4], [1], [2], [4]] Output: [1, 2, 2, 1, 1, -1, 4] ``` Keys 1 and 2 both reach a use count of 3, and key 2 was used less recently, so `put(3, 3)` evicts key 2. `put(2, 20)` evicts key 3, whose count is 1, and key 2 comes back with a count of 1 rather than its old count, so `put(4, 4)` evicts key 2 instead of key 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.

Examples

Input: (3, ['put', 'put', 'put', 'get', 'get', 'put', 'get', 'get', 'put', 'get', 'put', 'put', 'get', 'get', 'get'], [[1, 100], [2, 200], [3, 300], [2], [3], [4, 400], [1], [4], [5, 500], [2], [3, 333], [6, 600], [5], [3], [6]])

Expected Output: [200, 300, -1, 400, -1, -1, 333, 600]

Explanation: Source Example 1: never-read key 1 is evicted first, then key 2 as the least recently used of three keys tied at count 2, then key 5 at count 1.

Input: (2, ['put', 'get', 'put', 'get', 'get', 'get', 'put', 'put', 'put', 'get', 'get', 'get'], [[1, 1], [1], [2, 2], [2], [2], [1], [3, 3], [2, 20], [4, 4], [1], [2], [4]])

Expected Output: [1, 2, 2, 1, 1, -1, 4]

Explanation: Source Example 3: tie at count 3 evicts the less recently used key 2; key 2 comes back with count 1 and is evicted again by put(4, 4) instead of key 1.

Hints

  1. When the cache is full you need the key with the smallest use count and, among those, the least recently used one. Think about how to keep the keys organized so that key can be found without scanning the whole cache.
  2. A successful get and a put on a key that is already present both count as a use: the count goes up by 1 and the key becomes the most recently used. A get on a missing key changes nothing, and a put on a present key never evicts.
  3. An evicted key forgets its count. If it is put again later, it behaves exactly like a brand-new key with a count of 1.

Loading coding console...

Show the approach

Approach

Group the cached keys by use count. For every count c keep a list of the keys whose count is c, ordered from least to most recently used, plus each key's value and count, and track min_count, the smallest count present in the cache. A use (a get hit, or a put on a key already present) removes the key from list c and appends it to the tail of list c + 1; if list c becomes empty and c was min_count, min_count becomes c + 1, because the touched key now has count c + 1 and no key had a smaller count. A brand-new key is appended to list 1 and min_count is set to 1. When a new key arrives and the cache already holds capacity keys, the victim is the head of list min_count. Invariant: each list is in last-use order (every use moves a key to the tail of its new list), and min_count is the minimum count among cached keys whenever the cache is non-empty. So the head of list min_count has the smallest count and, among keys tied at that count, the oldest last use, which is exactly the key the rules evict. Every step touches a constant number of list nodes and map entries, so each operation is O(1) (average, because of hashing in Python). Edge cases: a get miss returns -1 and changes nothing (it neither occupies capacity nor moves any key); a put on an existing key never evicts, even at full capacity; an evicted key's count is discarded, so re-inserting it starts again at 1; with capacity 1 the sole key is evicted whatever its count; value 0 is an ordinary stored value, distinct from the -1 miss result; a sequence with no get returns an empty list. The Python reference uses a dict of OrderedDicts; the JavaScript, Java and C++ references build the same per-count doubly linked lists from arrays indexed by key (keys are at most 10^5) and by count (a count never exceeds the number of operations).

Time complexity:
O(n)
Space complexity:
O(capacity + n)