The problem
Design a cache with a fixed capacity that supports, in O(1) average time:
get(key): return the value, or −1 if absent. A successfulgetcounts as one use;put(key, value): insert or update the key (an update also counts as a use). If inserting a new key into a full cache, first evict the least frequently used key. If several keys tie on frequency, evict the least recently used of them.
capacity = 2
put(1, 1), put(2, 2)
get(1) -> 1 key 1 used twice, key 2 once
put(3, 3) full: evict key 2 (lowest count)
get(2) -> -1
get(3) -> 3 keys 1 and 3 both used twice now
put(4, 4) tie on count 2: evict key 1 (used less recently)
get(1) -> -1
get(3) -> 3
get(4) -> 4How to think about it
Start with brute force. Store key → (value, count, last_used_time). On eviction, scan every key for the smallest (count, last_used_time). Each get is O(1), but eviction is O(capacity). A heap ordered by (count, time) improves eviction to O(log n), but every get changes a key’s count, and updating an item deep inside a heap is awkward and still O(log n).
The key insight: group keys by frequency. Keep a map freq → bucket of keys with that count. When a key is used, move it from bucket f to bucket f + 1: two O(1) operations. Make each bucket an OrderedDict, so keys inside a bucket stay in the order they arrived. The oldest key in a bucket is then the least recently used among those with that count, which handles the tie-break for free.
The last piece: know which bucket to evict from. Keep minf, the smallest frequency currently present. It only changes in two predictable ways. A new key always has count 1, so after an insert minf = 1. When a use empties the bucket that was minf, the key that left it now has minf + 1, so minf goes up by exactly one. Eviction is then “pop the oldest key from buckets[minf]”, in O(1).
Which pattern is this? Design a data structure, the harder sibling of LRU: hash maps for lookup, ordered buckets for eviction order, and a pointer to the eviction bucket. The tell is “evict by frequency, ties by recency, all in O(1)“. Name your structures before writing code.
Approach
- Keep
val(key → value),freq(key → count),buckets(count →OrderedDictof keys), andminf. _touch(key): remove the key frombuckets[f]. If that bucket is now empty, delete it and, iffwasminf, increaseminf. Then set the count tof + 1and add the key to the end ofbuckets[f + 1].get(key): return −1 if absent; otherwise_touchit and return its value.put(key, value): if capacity is 0, do nothing. If the key exists, update its value and_touchit. Otherwise, if full, pop the oldest key frombuckets[minf]and delete it fromvalandfreq. Then insert the new key with count 1 intobuckets[1]and setminf = 1.
Solution
from collections import defaultdict, OrderedDict
class LFUCache:
def __init__(self, cap):
self.cap = cap; self.val = {}; self.freq = {}
self.buckets = defaultdict(OrderedDict); self.minf = 0 # freq -> keys in recency order
def _touch(self, key):
f = self.freq[key]; del self.buckets[f][key]
if not self.buckets[f]:
del self.buckets[f]
if self.minf == f: self.minf += 1 # min bucket emptied: min moves up by 1
self.freq[key] = f + 1; self.buckets[f + 1][key] = None # newest end of the next bucket
def get(self, key):
if key not in self.val: return -1
self._touch(key); return self.val[key]
def put(self, key, value):
if self.cap <= 0: return
if key in self.val:
self.val[key] = value; self._touch(key); return
if len(self.val) >= self.cap:
k, _ = self.buckets[self.minf].popitem(last=False) # LFU, oldest on ties
del self.val[k]; del self.freq[k]
self.val[key] = value; self.freq[key] = 1
self.buckets[1][key] = None; self.minf = 1 # new key: min frequency is 1
# cap=2: put(1,1) put(2,2) get(1)->1 put(3,3) get(2)->-1 get(3)->3 put(4,4) get(1)->-1 get(3)->3 get(4)->4Dry run
With capacity 2. Buckets list keys oldest first:
| Operation | Returns | freq |
Buckets | minf |
|---|---|---|---|---|
put(1,1), put(2,2) |
— | {1:1, 2:1} |
1:[1,2] |
1 |
get(1) |
1 | {1:2, 2:1} |
1:[2], 2:[1] |
1 |
put(3,3) |
— (evicts 2) | {1:2, 3:1} |
1:[3], 2:[1] |
1 |
get(2) |
−1 | unchanged | unchanged | 1 |
get(3) |
3 | {1:2, 3:2} |
2:[1,3] |
2 |
put(4,4) |
— (evicts 1) | {3:2, 4:1} |
2:[3], 1:[4] |
1 |
get(1) |
−1 | unchanged | unchanged | 1 |
get(3), get(4) |
3, 4 | {3:3, 4:2} |
3:[3], 2:[4] |
2 |
At get(3) the count-1 bucket empties, so minf moves to 2. At put(4,4), bucket 2 holds [1, 3] and key 1 is older, so it is evicted.
Complexity
- Time: O(1) average per operation. Every step is a hash-map or
OrderedDictinsert, delete or pop from the front, andminfchanges by a constant amount. - Space: O(capacity). Each cached key appears once in
val,freqand one bucket.
Edge cases and pitfalls
- Capacity 0:
putmust store nothing andgetalways returns −1. - Updating an existing key counts as a use and must not trigger an eviction.
- Resetting
minfto 1 on insert is required; forgetting it leavesminfpointing at a higher bucket and evicts the wrong key. - Evict before inserting. If you insert first, the new key (count 1) may be evicted immediately.
- Empty buckets. Delete a bucket when it empties, or
minflogic and memory both go wrong.
Related problems
- LRU Cache (LeetCode 146): the simpler sibling: evict purely by recency with a map and a linked list.
- Insert Delete GetRandom O(1) (LeetCode 380): another design where two structures must stay in sync on every call.
- Design HashMap (LeetCode 706): the hash table underneath every O(1) lookup used here.
- 1.Design HashMap (LeetCode 706)
- 2.Min Stack (LeetCode 155)
- 3.LRU Cache (LeetCode 146)
- 4.Insert Delete GetRandom O(1) (LeetCode 380)
- 5.Design Circular Queue (LeetCode 622)
- 6.Design Twitter (LeetCode 355)
- 7.Snapshot Array (LeetCode 1146)
- 8.Design Underground System (LeetCode 1396)
- 9.Product of the Last K Numbers (LeetCode 1352)
- 10.LFU Cache (LeetCode 460)