Heaps
Data Structures in Practice
Chapter 6 ยท Heaps
A priority queue hands back the item that matters most, not the one that arrived first or last. A heap is the structure that does this cheaply: it keeps just enough order to find the smallest item instantly, and does not waste effort sorting the rest. Python provides it in the heapq module.
What a Heap Gives You
heapq works on an ordinary Python list. The rule it maintains is that the item at index 0 is always the smallest, and every item is no larger than its two "children" at positions 2i+1 and 2i+2. Nothing else is promised: the list as a whole is not sorted.
The printed list is a valid heap but is not in order (8 comes before 5). Only h[0] is guaranteed. Popping repeatedly produces everything in sorted order, because each pop re-establishes the rule for what is left. The picture of that list as a tree:
Building and Reading a Heap
If you already have all the items, do not push them one by one. heapify rearranges a list in place in linear time.
| Operation | Call | Cost |
|---|---|---|
| Look at the smallest | h[0] | Constant |
| Add an item | heappush(h, x) | Logarithmic |
| Remove the smallest | heappop(h) | Logarithmic |
| Turn a list into a heap | heapify(h) | Linear |
| Find an arbitrary value | x in h | Linear (a heap is not searchable) |
| Fully sorted order | pop everything | n pops, so n log n |
h.sort() on something you will use as a heap (a sorted list happens to be a valid heap, but you will soon be tempted to insert or append directly, which silently breaks the rule). Change a heap only through heappush, heappop, heapreplace and heapify.
Max-Heaps by Negation
heapq only offers a min-heap. For "largest first" with numbers, store the negatives and flip the sign on the way out.
Priorities, Ties and Tuples
To attach a payload to a priority, push a tuple. Python compares tuples element by element, so the priority decides first and the payload only matters on a tie.
Two things went wrong quietly here. On a tie the payload strings were compared, so "test" beat "write" alphabetically rather than by arrival order. Worse, if the payloads cannot be compared at all, a tie raises an error:
The standard fix is a tie-breaker counter in the middle of the tuple: (priority, count, item). The count is unique and increasing, so comparison never reaches the item, and equal priorities come out in first-in, first-out order.
Top k Without Sorting Everything
"The three highest scores" does not need a full sort. nlargest and nsmallest take a key function like sorted does.
The idea behind them is worth knowing, because it works on streams too. Keep a min-heap holding the k best items seen so far. Its root is the weakest of the group, so a new item only matters if it beats the root, and then heapreplace swaps them in one step. Memory stays at k items however long the input is.
| Need | Best tool |
|---|---|
| Smallest or largest single item | min() / max() |
| A few best items (k small) | heapq.nlargest / nsmallest |
| Most of the items, or all, in order | sorted() |
| Repeatedly "the best now" while items keep arriving | a heap |
A Few More Tools
heapreplace(h, x)pops the smallest, then pushesx. The returned value may be larger thanx.heappushpop(h, x)pushesxfirst, then pops the smallest, so it returnsxitself ifxis already the smallest.heapq.merge(*sorted_inputs)lazily merges several already-sorted inputs into one sorted stream.
queue.PriorityQueue
The queue module from Data Structures in Practice 4 also has PriorityQueue. It is a heap wrapped in locks so several threads can share it safely. It uses the same tuple convention, with the same tie-breaking trap.
Use plain heapq in single-threaded code (it is lighter and gives you h[0] to peek) and PriorityQueue when threads share the queue.
Scheduling, and Cancelling Lazily
Schedulers, "next event" simulations, Dijkstra's shortest paths (Data Structures in Practice 8) and merging many sorted files are all the same pattern: repeatedly take the most urgent item while new ones keep arriving. One catch is that you cannot cheaply remove an item from the middle of a heap. The usual answer is lazy deletion: mark the entry as cancelled, and discard it when it eventually reaches the top. Exercise 3 builds this.
bisect), a dict or a tree (Data Structures in Practice 7).
Challenges
Write a class KthLargest(k) with an add(x) method that accepts a stream of numbers and returns the current k-th largest value (or None until k values have arrived). Use a min-heap of at most k items. Feed it 4, 5, 8, 2, 10, 1, 9 with k = 3 and print each result.
Write heap_sort(items) using only heapify and heappop (it must not change the input), and merge_sorted(*lists) that merges any number of sorted lists by keeping one candidate per list in a heap. Check the second against heapq.merge, and test it with an empty list and with no arguments.
Build a Scheduler class with add(priority, name) (returning a ticket), cancel(ticket) and next_task() (returning None when empty). Lower numbers run first, equal priorities run in the order added, and cancelled tasks never run. Cancel lazily. Show it working with five tasks, one of them cancelled.
Quick Reference
- Heap: a list where
h[0]is always the smallest; the rest is only partly ordered - Costs: peek constant; push and pop logarithmic; heapify linear
- Max-heap: store negatives (numbers) and flip the sign back
- Payloads: push
(priority, count, item)so ties are first-in, first-out and items are never compared - Top k:
nlargest/nsmallest, or a size-k min-heap for streams - Others:
heapreplace,heappushpop,heapq.merge,queue.PriorityQueuefor threads - Cancelling: mark and skip when it surfaces (lazy deletion)
- Not for: searching, arbitrary removal or in-order walking