Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our module on implementing core data structures.
In the previous lessons, we built an LRU cache from the ground up and then compared it to a simpler version using collections.OrderedDict. We concluded that while OrderedDict is great for production simplicity, the from-scratch implementation is essential for demonstrating deep data structure knowledge in an interview. The core principle of LRU is recency: it assumes that recently accessed items are likely to be accessed again soon.
Today, we'll tackle a more complex caching policy: Least Frequently Used (LFU). This policy operates on a different assumption: the total number of times an item has been accessed (its frequency) is the best predictor of its future use, regardless of when it was last accessed. This seemingly small change in requirements leads to a significant increase in implementation complexity.
Our learning outcome is to implement an LFU cache and articulate the scenarios where it is more suitable than an LRU cache. This will test your ability to work with composite data structures and to reason about system design trade-offs, both crucial skills for a role at Anthropic.
1. When to Use LFU vs. LRU
Before diving into code, it's critical to understand the "why." A caching policy is a solution to a specific access pattern. Choosing the wrong one can hurt performance.
- LRU (Least Recently Used) excels with temporal locality. It's perfect for workloads where access patterns are bursty or sequential, like processing a stream of recent events or browsing a news feed. Its weakness is "scan resistance": a one-time full scan of a dataset can flush out valuable, older items from the cache.
- LFU (Least Frequently Used) excels with stable popularity. It's designed for workloads with a highly skewed access distribution, where a small subset of items accounts for the vast majority of hits over the long term.

Imagine a system like AWS Bedrock serving model metadata. A popular model like claude-3-opus-20240229 might be queried constantly throughout the day. Even if there's a 30-minute period where other, less common models are accessed (a "scan"), you wouldn't want Claude 3's metadata to be evicted. LFU would keep it cached due to its high overall access count, whereas LRU might evict it in favor of the more recently accessed, but less popular, models.
To solidify this concept, let's watch a brief clip from a system design discussion.
Caching in System Design Interviews w/ Meta Staff Engineer
The video "Caching in System Design Interviews" from Hello Interview explains the LFU policy and the types of access patterns where it makes sense.
Please watch from 01:59 to 02:44. Pay close attention to the explanation of why LFU is suitable for "highly skewed" access patterns.
However, LFU has a key weakness: cache pollution. An item that was once extremely popular but is no longer relevant can remain in the cache for a long time, displacing newer, more relevant items. This is often called the "stickiness" problem. We'll revisit this when discussing trade-offs.
2. The Challenge: Designing an O(1) LFU Cache
The formal problem statement is as follows:
- Design and implement a data structure for a Least Frequently Used (LFU) cache.
- It must support
get(key)andput(key, value)operations. - When the cache reaches capacity, a
putoperation must evict the least frequently used item. - If there's a tie (multiple items have the same lowest frequency), the least recently used item among them should be evicted.
- Both
getandputmust have an average time complexity of O(1).
The O(1) constraint and the tie-breaker rule make this significantly harder than the LRU cache. A simple hash map and a single doubly linked list won't work because we need to track both frequency and recency. A naive approach using a min-heap to track frequencies would result in O(log N) operations, which doesn't meet the requirement.
To achieve O(1), we need a more sophisticated, composite data structure.
3. The O(1) LFU Architecture
The standard solution involves combining two hash maps and multiple doubly linked lists. The core idea is to group items by their access frequency.
-
key_to_node(dict): A primary hash map that maps akeyto its correspondingNodein the cache. This providesO(1)lookup for any item. TheNodeitself will store itskey,value, andfreq(frequency). -
freq_to_list(dict): A second hash map that maps afrequencycount (e.g.,5) to a doubly linked list containing all items that have been accessed exactly that many times.- This is the "list of lists" concept.
freq_to_list[5]is a DLL of all items with frequency 5.freq_to_list[1]is a DLL for all items with frequency 1. - Within each frequency-specific DLL, we maintain an LRU policy. New items are added to the head, and eviction candidates are taken from the tail. This elegantly handles the tie-breaker rule.
- This is the "list of lists" concept.
-
min_freq(int): An integer variable that tracks the lowest frequency currently present in the cache. This is a critical optimization that allows us to find the list of eviction candidates inO(1)time, without having to iterate through thefreq_to_listmap.

To see how these pieces fit together, let's watch a detailed explanation of the architecture.
LFU Cache - Leetcode 460 - Python
The video "LFU Cache - Leetcode 460 - Python" from NeetCodeIO provides an excellent conceptual breakdown of this architecture before diving into the code.
Watch from the beginning to 05:10. This will cover the problem statement and the high-level design involving a map from counts to doubly linked lists.
4. Implementing the LFU Cache
Now, let's translate this architecture into production-quality Python code. We'll build this incrementally, starting with our helper classes and then the main LFUCache orchestrator. The following article provides a very clean and well-documented implementation that we will use as our reference.
LFU Cache (Least Frequently Used) - Arun Baby
The article "LFU Cache (Least Frequently Used)" by Arun Baby is an exceptional resource. It not only provides a full implementation but also delves into the design rationale, complexity, and real-world trade-offs.
First, read section 5, "Implementation in Python," which provides the code for the Node, DoublyLinkedList, and LFUCache classes. We will analyze the logic of the get and put methods in detail below.
Let's break down the implementation from the article.
Helper Classes: Node and DoublyLinkedList
The Node is slightly different from our LRU version, as it now includes a freq attribute, initialized to 1. The DoublyLinkedList is nearly identical to what we built for our from-scratch LRU cache. It uses dummy head/tail nodes to simplify add_to_front and remove operations and includes a pop_tail method for O(1) LRU eviction.
class Node:
"""A single node representing a cache entry."""
def __init__(self, key, value):
self.key = key
self.val = value
self.freq = 1
self.prev = None
self.next = None
class DoublyLinkedList:
"""A standard DLL with dummy head and tail for easy removal."""
def __init__(self):
# ... implementation is the same as our from-scratch LRU ...
# (add_to_front, remove, pop_tail)
LFUCache Orchestrator
This class brings all the components together. The logic is intricate, so let's walk through the key operations as described in the video and article.
The core of the logic resides in a helper function, which the article calls _update_node. This function is responsible for "promoting" a node to a higher frequency tier upon access.
_update_node(self, node) logic:
- Get the node's current frequency,
old_freq. - Remove the node from the list at
self.freq_to_list[old_freq]. - Crucial check: If the list at
old_freqis now empty ANDold_freqwas theself.min_freq, we must incrementself.min_freq. This ensures our eviction shortcut remains valid. - Increment the node's frequency:
node.freq += 1. - Add the node to the front of the list at its
new_freq. If this list doesn't exist, create it.
get(self, key) logic:
- If the
keyis not inself.key_to_node, return -1. - Retrieve the
nodefromself.key_to_node. - Call
self._update_node(node)to promote it. - Return
node.val.
put(self, key, value) logic:
This is the most complex part, handling both updates and insertions with eviction.
- Case 1: Key exists.
- Retrieve the
nodefromself.key_to_node. - Update its value:
node.val = value. - Call
self._update_node(node)to promote it.
- Retrieve the
- Case 2: New key.
- Check capacity. If
self.size >= self.capacity:- Find the list of candidates for eviction:
eviction_list = self.freq_to_list[self.min_freq]. - Pop the LRU item from this list:
victim = eviction_list.pop_tail(). - Remove the
victimfromself.key_to_node. - Decrement
self.size.
- Find the list of candidates for eviction:
- Create a
new_node. - Add it to
self.key_to_node. - Add the
new_nodeto the front of the list at frequency 1 (self.freq_to_list[1]). Create the list if it doesn't exist. - Crucial step: Reset
self.min_freq = 1. A new item's frequency is always 1, making it the new minimum. - Increment
self.size.
- Check capacity. If
The following part of the NeetCode video walks through this implementation logic, which can help clarify how the pieces move.
LFU Cache - Leetcode 460 - Python
Let's continue with the NeetCodeIO video to see how the get and put methods are implemented, including the eviction logic.
Watch from 06:48 to 14:17. This covers the get method, the counter helper function (our _update_node), and the put method with its eviction logic. Pay special attention to how min_freq is managed.
5. Production-Grade Considerations
Your work on Bedrock's infrastructure highlights the gap between a whiteboard algorithm and a production system. The LFU implementation is a perfect place to discuss this. While our implementation is correct, a real-world system would require more.
- Concurrency: Our current implementation is single-threaded. Making it thread-safe is the topic of our next lesson, but it would involve adding locks. A simple global lock would create high contention. A more advanced approach, like the "lock stripping" used in Java's
ConcurrentHashMap, would involve sharding the cache and locking individual shards, trading global LFU accuracy for higher throughput. - Probabilistic Counting: For massive caches, storing an exact frequency count for every item is memory-intensive. Systems like Redis and high-performance caches often use probabilistic data structures like a Count-Min Sketch. This approximates frequencies using sub-linear space, trading perfect accuracy for huge memory savings. The article "Designing an in-memory LFU cache" (LINK) explores this in detail, and it's a technique worth knowing about for system design discussions.
- Frequency Aging: The "stickiness" problem is real. To solve it, production systems implement frequency aging. For example, all frequency counts might be halved periodically. This allows the system to adapt to changing access patterns, as the impact of old accesses decays over time. The Window-TinyLFU algorithm, used in many modern caches, is an advanced form of this that combines an LRU "window" with an LFU main area to get the best of both worlds.
Conclusion
In this lesson, we've gone deep into one of the most complex standard data structure interview problems. You should now be able to implement an O(1) LFU cache and, just as importantly, articulate its design trade-offs.
Key Takeaways:
- LFU vs. LRU: LFU is for stable, long-term popularity (skewed access), while LRU is for temporal locality (bursty access).
- O(1) Architecture: The solution hinges on a composite data structure: a
key_to_nodemap for direct access, afreq_to_listmap to group nodes by frequency, and amin_freqinteger forO(1)eviction. - Tie-Breaking: LRU is used as a tie-breaker within each frequency group by managing each group's nodes in a doubly linked list.
- Complexity: LFU is significantly more complex to implement and has a higher constant-factor overhead than LRU, which is a critical trade-off to consider in a production system.
Next Lesson Preview
We've successfully built a single-threaded LFU cache. However, any real-world service, like the ones you build at AWS, is highly concurrent. In our next lesson, we will implement a thread-safe LFU cache and discuss the increased complexity compared to a thread-safe LRU cache. This will involve a deep dive into locking strategies and their performance implications, connecting directly to your experience with concurrent systems in Rust and preparing you for advanced concurrency questions.