LFU Cache
O(1) cache evicting the least frequently used key; ties broken by least recent.
Open on LeetCode ↗Intuition
Group keys by use-count: freq → an ordered set of keys (insertion order = recency). Track the minimum frequency. A touch moves a key from bucket f to f+1; eviction pops the oldest key from the min-freq bucket. Every step is a dict/OrderedDict operation — O(1).
LFU needs two orderings at once — by frequency, and by recency within a frequency. A dict of frequency to ordered-dict gives both, and tracking minf makes eviction O(1) instead of a scan for the smallest count. The key realisation: minf only ever increases by one, and only when the bucket it points at empties.
Approach
Two maps + a counter
vals key→(value,freq); buckets freq→OrderedDict of keys; minf = current lowest frequency.
Touch promotes
Remove key from bucket f; add to bucket f+1. If bucket f was the min and is now empty, minf += 1 (the only way minf rises).
Insert resets minf
A new key has freq 1 → minf = 1. Eviction (before insert, when full): popitem(last=False) on bucket minf = oldest of the least-used.
Solution & live demo
Common pitfalls
Scanning for the minimum frequency on eviction
f = min(self.buckets) old, _ = self.buckets[f].popitem(last=False)
old, _ = self.buckets[self.minf].popitem(last=False)
That's O(number of distinct frequencies) per eviction, breaking the O(1) requirement. Maintaining minf incrementally is possible because a promotion can only empty the current minimum bucket, in which case the new minimum is exactly one higher.
Not resetting minf to 1 on insert
self.vals[key] = [value, 1] self.buckets[1][key] = None
self.vals[key] = [value, 1] self.buckets[1][key] = None self.minf = 1
A brand-new key has frequency 1, which is the global minimum by definition. Leaving minf higher makes the next eviction look in an empty or wrong bucket and throw, or evict a more valuable entry.
Treating an update as a plain overwrite
if key in self.vals:
self.vals[key][0] = value
returnif key in self.vals:
self.vals[key][0] = value
self._touch(key); returnput on an existing key counts as a use, so its frequency must rise like a get. Skipping the touch leaves the entry looking colder than it is and it gets evicted ahead of genuinely stale keys.
Edge cases
Value update counts as a use — promote, don't evict.
Every put is a no-op; guard first.