Data structures and storage engines
Heaps and top-k
How a binary heap keeps the smallest or largest item at the top, and how a heap of size k finds the top k in one pass without sorting everything.
A binary heap is a tree stored in an array, with one rule: each parent is smaller than its children (a min-heap) or larger (a max-heap). The root is always the smallest or largest item. Pushing an item and popping the root each take O(log n), and reading the root takes O(1).
Heaps matter most for a job that comes up constantly: "give me the top k of a large stream," such as the 10 slowest requests, the 5 biggest orders or the next task due.
The big idea
Sorting a million items to find the top ten costs about n log n. A heap of size k finds them in about n log k, which for k = 10 is barely more than a single scan. And it uses O(k) memory, so it works on a stream you can't store.
The trick is to keep a min-heap of size k. Its root is the smallest of the top k so far. Each new item either beats that root, and then replaces it, or it doesn't, and then it's ignored.
Walking through it
import heapq
def top_k(items, k):
heap = [] # min-heap of the k largest seen so far
for x in items:
heapq.heappush(heap, x)
if len(heap) > k:
heapq.heappop(heap) # drop the smallest
return sorted(heap, reverse=True)
print(top_k(range(1, 11), 3))Output:
[10, 9, 8]Each item is pushed once. The heap never grows beyond k + 1 items, so the work per item is O(log k). When the heap has more than k items, the smallest is removed. At the end, the heap holds exactly the k largest values, and sorting that small heap gives them in order.
For a stream, the same loop runs without storing the input. For a leaderboard that is updated over time, keep the heap and push each new score.
Tip
In Python, heapq.nlargest(k, items) does the same job with less code. Write the loop yourself when the item type or the scoring needs custom handling.
Heaps in systems
Heaps show up in schedulers that run the next timer or the next job by due time, in Dijkstra's shortest-path algorithm for the closest unvisited node, and in merging sorted streams. Timers in event loops, covered in event loops, are usually a heap or a timing wheel, so that the next deadline is always at the root.
Common mistakes
- Sorting the whole input to get the top k, when the input is large or streamed.
- Using a max-heap for top-k, which holds the wrong items. Use a min-heap of size k, so the root is the cutoff.
- Forgetting that a heap doesn't support finding an arbitrary item quickly. For that, use an indexed structure or a tree.
Check yourself
- Why does a min-heap of size k give the top k, and not the bottom k? Answer: its root is the smallest of the k kept items, so any larger new item can replace it. The kept items are the largest seen.
- Why is top-k with a heap cheaper than sorting? Answer: each item costs O(log k) against the heap, instead of O(log n) for a full sort, and the memory stays at k items.
Key takeaways
- A binary heap keeps the smallest or largest item at the root in O(1), with O(log n) updates.
- Top-k uses a min-heap of size k, so it finds the largest k in one pass.
- The memory stays at k items, which suits streams.
Further learning
Next, see sorted trees, skip lists and tries for structures that keep keys ordered for range queries and prefix searches.
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.
Arrays and memory layout
Why contiguous arrays are fast on real hardware, how cache lines and prefetching reward sequential access, and how a fixed-size ring buffer handles streams with bounded memory.
Sorted trees, skip lists and tries
The structures that keep keys in order for range queries, the red-black and skip list options for in-memory sorting, and tries for prefix search.