End-to-End State Machine of the $O(1)$ LFU Engine
Let us trace every edge case required to implement a zero-allocation, bug-free $O(1)$ LFU Cache of capacity $C$. Every cached item is represented by a Node containing {key, value, freq, prev, next}. We maintain:
key_table: Mapskey -> Node*(or arena slot index).freq_table: Mapsfreq -> DoublyLinkedList, where each list orders nodes of that exact frequency from Most Recently Used (front) to Least Recently Used (back).min_freq: An integer tracking the smallest frequency $f \ge 1$ such thatfreq_table[f]is non-empty.
- Transition 1 — Cache Hit on
get(key): Find node $u$ with frequency $f = u.\text{freq}$. Unlink $u$ fromfreq_table[f]. Iffreq_table[f].empty()and $f == \text{min\_freq}$, set $\text{min\_freq} = f + 1$. Set $u.\text{freq} = f + 1$ and push $u$ to the MRU front offreq_table[f + 1]. Return $u.\text{value}$. - Transition 2 — Cache Hit on
put(key, new_val): Overwrite $u.\text{value} = \text{new\_val}$, and execute the exact same frequency promotion logic as Transition 1! - Transition 3 — Cache Miss on
put(key, val)when Full (size == capacity): Look up bucketfreq_table[min_freq]. Pop its LRU tail node $v$, and erase $v.\text{key}$ fromkey_table. (Note: we do NOT need to updatemin_freqbased on $v$'s removal because we are about to resetmin_freq = 1in Transition 4!) - Transition 4 — Inserting the New Node on
put(key, val): Initialize node $w$ with $\text{freq} = 1$, push $w$ to the MRU front offreq_table[1], insertkey_table[key] = w, and unconditionally setmin_freq = 1.
Operation Complexity & Edge-Case Matrix
| Operation / Edge Case | min_freq Mutation |
Bucket Lists Touched | Time Complexity | Common Implementation Bug |
|---|---|---|---|---|
get(key) (Hit at $f == \text{min\_freq}$, last item in bucket) |
min_freq = f + 1 |
Unlink from $f$, push front $f+1$ | $O(1)$ | Forgetting to check if bucket $f$ became empty |
get(key) (Hit at $f > \text{min\_freq}$) |
Unchanged | Unlink from $f$, push front $f+1$ | $O(1)$ | Incrementing min_freq when $f \neq \text{min\_freq}$! |
put(key, val) (New key when full) |
Reset min_freq = 1 |
Pop tail of min_freq, push front of $1$ |
$O(1)$ | Evicting AFTER setting min_freq = 1! |
capacity == 0 |
N/A (No-op) | None | $O(1)$ | Attempting to evict from an empty freq_table[0] |
Visualizing Eviction + Insertion Order on Cache-Full Miss
Complete Control Flow of LFUCache::put(key, value)
Call touch(node) to promote freq"] D -- "No (Miss)" --> F{"size == capacity?"} F -- "Yes (Full)" --> G["Evict LRU tail of freq_table[min_freq]
Erase victim.key from key_table"] F -- "No" --> H["Allocate/Reuse Node(key, val, freq=1)
Push to MRU front of freq_table[1]
Set min_freq = 1"] G --> H
std::list Allocator ThrashingA textbook C++ LFU implementation using std::unordered_map<int, std::list<Node>> freq_table suffers from two performance hazards in long-running services:
- Per-Promotion Heap Allocations: Calling
freq_table[f].erase(it)andfreq_table[f+1].push_front(...)deallocates a heap node and allocates a brand-new heap node on every single cache hit! Usingstd::list::splice(or intrusive doubly linked list nodes) moves the existing node between frequency buckets by rewiring 4 pointers with zero heap allocations. - Unbounded Empty Frequency Buckets: If a hot item is accessed $10^7$ times, a naive
freq_tablecreates $10^7$ emptystd::listentries for frequencies $1, 2, \dots, 10^7 - 1$, exhausting gigabytes of RAM! In production, either erase empty buckets fromfreq_tablewhen their size hits 0, or store frequency buckets as an intrusive doubly linked list of active frequency headers.
Step through O(1) cache hits (splicing nodes to MRU head or promoting to freq+1 bucket list) and O(1) capacity evictions from the LRU tail / minFreq bucket.
Invariant & State
Production Implementation: Zero-Allocation Intrusive O(1) LFU Cache Engine
#include <cstdint>
#include <unordered_map>
#include <vector>
class LFUCache {
struct Node {
int key = 0;
int value = 0;
int freq = 1;
int prev = -1;
int next = -1;
};
struct FreqList {
int head = -1; // MRU front
int tail = -1; // LRU back
int count = 0;
};
int capacity_;
int size_ = 0;
int min_freq_ = 0;
std::vector<Node> pool_;
std::vector<int> free_slots_;
std::unordered_map<int, int> key_to_slot_;
std::unordered_map<int, FreqList> freq_lists_;
void remove_from_list(int slot, int freq) {
auto it = freq_lists_.find(freq);
FreqList& list = it->second;
const int p = pool_[slot].prev;
const int n = pool_[slot].next;
if (p != -1) pool_[p].next = n;
else list.head = n;
if (n != -1) pool_[n].prev = p;
else list.tail = p;
pool_[slot].prev = pool_[slot].next = -1;
if (--list.count == 0) {
freq_lists_.erase(it); // Prevent unbounded empty bucket memory leak!
if (min_freq_ == freq) {
++min_freq_;
}
}
}
void push_front_list(int slot, int freq) {
FreqList& list = freq_lists_[freq];
pool_[slot].prev = -1;
pool_[slot].next = list.head;
if (list.head != -1) pool_[list.head].prev = slot;
else list.tail = slot;
list.head = slot;
++list.count;
}
void touch(int slot) {
const int f = pool_[slot].freq;
remove_from_list(slot, f);
pool_[slot].freq = f + 1;
push_front_list(slot, f + 1);
}
public:
explicit LFUCache(int capacity) : capacity_(capacity), pool_(capacity > 0 ? capacity : 0) {
for (int i = capacity - 1; i >= 0; --i) {
free_slots_.push_back(i);
}
}
int get(int key) {
auto it = key_to_slot_.find(key);
if (it == key_to_slot_.end()) return -1;
const int slot = it->second;
touch(slot);
return pool_[slot].value;
}
void put(int key, int value) {
if (capacity_ <= 0) return;
auto it = key_to_slot_.find(key);
if (it != key_to_slot_.end()) {
const int slot = it->second;
pool_[slot].value = value;
touch(slot);
return;
}
int slot = -1;
if (size_ == capacity_) {
// Evict LRU tail of min_freq_ BEFORE resetting min_freq_ = 1!
const int victim = freq_lists_[min_freq_].tail;
remove_from_list(victim, min_freq_);
key_to_slot_.erase(pool_[victim].key);
slot = victim; // Reuse evicted slot in place!
} else {
slot = free_slots_.back();
free_slots_.pop_back();
++size_;
}
pool_[slot].key = key;
pool_[slot].value = value;
pool_[slot].freq = 1;
push_front_list(slot, 1);
key_to_slot_[key] = slot;
min_freq_ = 1;
}
};
When implementing LFUCache::put on a cache miss when size == capacity, you must always evict the victim from freq_lists_[min_freq_] BEFORE inserting the new key and resetting min_freq_ = 1. If you insert the new key first (or set min_freq_ = 1 first), and the old min_freq_ was $> 1$, then evicting from min_freq_ == 1 will immediately evict the brand-new item you just inserted! Additionally, always erase a frequency bucket from freq_lists_ when its count drops to 0 so long-running caches do not leak millones of empty hash entries as item frequencies climb.