Problem Statement
LFU Cache
We are building a cache. A cache is a small box that holds a limited number of items so we can grab them fast. This one is an LFU cache, which stands for Least Frequently Used. The rule is simple: when the box is full and we need room for something new, we throw out the item that has been used the fewest times. "Used" means asked for or set. If two items are tied for fewest uses, we throw out the one we have not touched in the longest time. The catch is that every action has to be fast, O(1), which means the work does not grow when the cache grows. To pull that off we keep three lookup tables side by side: one from each key to its value, one from each key to how many times it has been used, and one from each use-count to the list of keys that currently sit at that count. A lookup table (a map or dictionary) is like a coat-check: you hand over a tag and instantly get back the matching coat, no searching.
Signals to notice
Brute force first
Scan all entries for minimum frequency on eviction — O(n) per put.
The key insight
Hash map (key → node) + frequency buckets (freq → doubly linked list). Track minFreq. On access, promote node to freq+1. On eviction, remove LRU from minFreq bucket. All O(1).
Trace it on cap=2; put(1,1) put(2,2) get(1) put(3,3) get(2) get(1) put(4,4)
put(1,1): new -> val{1:1} freq{1:1} bucket{1:[1]} minFreq=1
put(2,2): new, room -> val{1:1,2:2} bucket{1:[1,2]} minFreq=1
get(1)->1: promote 1 to freq2 -> bucket{1:[2],2:[1]} minFreq stays 1
put(3,3): full -> evict bucket[minFreq=1] LRU = key 2 -> bucket{1:[3],2:[1]} minFreq=1
get(2)->-1: not in cache, no change
get(1)->1: promote 1 to freq3 -> bucket{1:[3],3:[1]} minFreq=1
put(4,4): full -> evict bucket[1] LRU = key 3 -> val{1:1,4:4} bucket{1:[4],3:[1]} minFreq=1What must stay true
Each frequency level has its own LRU list. minFreq points to the lowest non-empty bucket. Eviction removes the LRU from that bucket — least frequent, and among those, least recently used.
Shape of the loop
_update(key): # called on every access f = keyFreq[key]; keyFreq[key] = f+1 freqKeys[f].remove(key) # LRU-ordered bucket if freqKeys[f] empty and f == minFreq: minFreq += 1 freqKeys[f+1].add(key) # promote to next bucket put(key,val): if full -> evict freqKeys[minFreq].popLRU(); insert at freq 1; minFreq = 1
Pseudocode only — the full worked solution lives in the Solution tab.
Easy way to go wrong
Not resetting minFreq on new key insertion — new keys always enter at freq=1, so minFreq resets to 1 on every new key.