Data structures and storage engines
LRU caches
How an LRU cache evicts the least recently used item in O(1) by pairing a hash map with a doubly linked list, and why a plain array or sorted list is too slow.
A least-recently-used (LRU) cache holds a fixed number of items. When it's full and a new item arrives, it evicts the item that was used longest ago. The cache needs two operations to be fast: look up a key, and move a key to the front when it's used. Doing both in O(1) takes two structures working together.
LRU is the eviction policy behind many caches, from browser and CDN caches to the buffer pool inside a database. Its effectiveness depends on the workload, as covered in caching.
The big idea
A hash map answers "where is this key?" in O(1). It can't tell which key is oldest. A doubly linked list keeps items in order of use, with the most recent at the front and the oldest at the back. Moving a node to the front or removing the back is O(1) if you hold a direct pointer to the node.
Combine them: the hash map gives the node for a key, and the list gives the order. Each operation touches only a few pointers.
Walking through it
Python's OrderedDict is a hash map that also keeps insertion order and supports moving a key to the end. That makes it a complete LRU:
from collections import OrderedDict
class LRU:
def __init__(self, cap):
self.cap = cap; self.d = OrderedDict()
def get(self, k):
if k not in self.d: return None
self.d.move_to_end(k); return self.d[k]
def put(self, k, v):
self.d[k] = v; self.d.move_to_end(k)
if len(self.d) > self.cap: self.d.popitem(last=False)
c = LRU(2)
c.put("A", 1); c.put("B", 2); c.get("A"); c.put("C", 3)
print(list(c.d))Output:
['A', 'C']With capacity 2, A and B fill the cache. Reading A makes it most recent, so B becomes the oldest. Adding C evicts B, leaving A and C. OrderedDict does this in O(1) per operation, using a linked list internally.
In a language without a built-in, the same design uses a hash map from key to node, and a doubly linked list of nodes. Expect this in interviews, and in production code when the library doesn't provide one.
Trade-offs
LRU is simple, but it's not always the best policy:
- A full scan through a dataset touches each item once, and pushes out hot items. The buffer pool design in buffer pool exists to handle this.
- LFU, which evicts the least frequently used, handles scans better, but it's slower to adapt when popularity changes.
- Time-based expiry suits data that goes stale regardless of use.
Note
A cache that's shared across threads needs a lock around the map and the list, or the two can disagree. Use a concurrent cache library when that matters.
Common mistakes
- Implementing LRU with a plain array, so each access is O(n) to find and move an item.
- Forgetting that a
getalso counts as a use, so a hot key gets evicted when it shouldn't be. - Using LRU for a workload dominated by scans, where it evicts the useful items.
Check yourself
- Why does the hash map alone not give LRU? Answer: it finds a key quickly, but it doesn't record which key was used least recently.
- Why is a doubly linked list needed, and not a single linked list? Answer: removing a node from the middle needs its predecessor, and a doubly linked list keeps that pointer, so the removal stays O(1).
Key takeaways
- LRU evicts the least recently used item, and it needs a hash map plus an ordered list.
- The hash map finds the node in O(1), and the list moves it in O(1).
- Reads count as use. Scans can flush useful items, so consider other policies for scan-heavy work.
Further learning
Next, see graphs and topological sort for ordering tasks with dependencies.
Related
Data structures in practice
Choosing data structures by memory layout, access pattern and the operations that matter, with the common ones for arrays, hash maps, heaps, trees, caches and graphs.
Hash maps
How hash maps map keys to buckets, how load factor and collisions set their speed, and why adversarial keys can turn O(1) lookups into O(n).
Buffer pool
The buffer pool caches database pages in RAM. How eviction chooses which page to drop, why sequential scans can wipe out the cache, and how midpoint insertion protects hot pages.
Caching
Where to put a cache, how to read and write through it, how to size it from hit-rate math, and how to keep it from serving stale data or melting down.