Heaps & Priority Queues
Overviewโ
A priority queue answers one question: what is the most important item right now? A binary heap is the standard way to implement it โ a tree whose only ordering rule is that a parent outranks its children.
That rule is deliberately weaker than a BST's. A heap cannot tell you whether 42 is present, and cannot iterate in sorted order. It only knows its extreme, and in exchange it is cheap to maintain and needs no pointers at all.
Core Conceptsโ
| Term | Meaning |
|---|---|
| Heap property | Every parent โฅ its children (max-heap), or โค (min-heap) |
| Shape property | The tree is complete โ every level full except the last, filled left to right |
| Sift up (bubble up) | Restore the property after inserting at the bottom |
| Sift down (heapify) | Restore the property after replacing the root |
| Priority queue | The interface; a heap is the usual implementation |
Note what the heap property does not say: siblings are unordered, and a node deep in one subtree may be larger than a shallow node in another. Only the root is guaranteed to be the extreme.
Architecture / Mechanismโ
The array trickโ
The shape property means a heap has no holes, so it can live in a flat array with the tree structure implied by arithmetic โ no left/right pointers, and perfect cache locality:

parent(i) = (i - 1) // 2
left(i) = 2i + 1
right(i) = 2i + 2
The two operationsโ
Both work by moving one element along a single root-to-leaf path, which is why both are O(log n):
def push(heap, value):
heap.append(value) # place at the end โ keeps the shape
i = len(heap) - 1
while i > 0: # sift up while it outranks its parent
parent = (i - 1) // 2
if heap[parent] >= heap[i]:
break
heap[parent], heap[i] = heap[i], heap[parent]
i = parent
def pop_max(heap):
top = heap[0]
heap[0] = heap[-1] # move the last element to the root
heap.pop()
i, n = 0, len(heap)
while True: # sift down while a child outranks it
largest = i
for child in (2 * i + 1, 2 * i + 2):
if child < n and heap[child] > heap[largest]:
largest = child
if largest == i:
break
heap[i], heap[largest] = heap[largest], heap[i]
i = largest
return top
| Operation | Cost |
|---|---|
| Peek at the extreme | O(1) |
| Insert | O(log n) |
| Extract the extreme | O(log n) |
| Build a heap from n items | O(n) โ not O(n log n) |
| Search for an arbitrary value | O(n) |
Sifting down from every non-leaf node, working backwards from the middle of the array, costs O(n)
rather than O(n log n). The reason is that most nodes are near the bottom and barely move: half the
nodes are leaves and cost nothing, a quarter can sift at most one level, and so on. The sum
ฮฃ n/2^(k+1) ยท k converges to n. Use heapq.heapify(list) rather than pushing n times.
Practical Usageโ
import heapq
# Python's heapq is a MIN-heap operating in place on a plain list
heap = [5, 1, 8, 3]
heapq.heapify(heap) # O(n)
heapq.heappush(heap, 2) # O(log n)
smallest = heapq.heappop(heap) # O(log n)
# For a max-heap, negate โ heapq has no key or reverse parameter
heapq.heappush(heap, -value)
# Pair (priority, item) tuples; add a counter to break ties so that
# unorderable items are never compared
import itertools
counter = itertools.count()
heapq.heappush(pq, (priority, next(counter), task))
# Top-k without sorting everything: O(n log k), not O(n log n)
top_10 = heapq.nlargest(10, huge_list)
Where priority queues show up:
- Dijkstra's algorithm โ repeatedly take the nearest unvisited node. This is the single most important use.
- Heapsort โ build a heap, then extract the maximum n times.
- OS scheduling โ pick the highest-priority runnable task, as in process scheduling.
- Event simulation โ process events in timestamp order as new ones are generated.
- Streaming top-k โ keep a size-k min-heap; anything smaller than its root cannot make the cut. This uses O(k) memory regardless of stream length.
- Merging k sorted sequences โ a heap over the k heads gives the next element in O(log k).
Edge Cases & Pitfallsโ
heapqis a min-heap with noreverse=option. Negate values, or wrap them in a class with inverted comparison. Forgetting this is the most common heap bug in Python.- Tuples compare element by element, so
(priority, task)will comparetaskwhen priorities tie โ and raiseTypeErrorif tasks are not orderable. Insert a monotonic counter between them. - You cannot efficiently change or remove an arbitrary element. Finding it is O(n). The standard workaround is lazy deletion: mark the entry invalid, push a replacement, and discard stale entries when they surface at the top.
- A heap is not sorted. Printing the backing array shows a valid heap that looks unsorted, because it is. Only repeated extraction produces order.
heapqis not thread-safe. Usequeue.PriorityQueuefor concurrent access.
Comparisonsโ
| Heap | Sorted array | Balanced BST | |
|---|---|---|---|
| Peek extreme | O(1) | O(1) | O(log n) |
| Insert | O(log n) | O(n) | O(log n) |
| Extract extreme | O(log n) | O(1) at one end | O(log n) |
| Find arbitrary | O(n) | O(log n) | O(log n) |
| Sorted iteration | No | Yes | Yes |
| Memory overhead | None โ a plain array | None | Two pointers per node |
Use a heap when you only ever want the extreme; a balanced tree when you also need ordering or arbitrary lookup.
Referencesโ
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 6 โ heaps, heapsort, and the O(n) build-heap analysis.
- CPython
heapqsource โ the module's docstring is an unusually good explanation of the invariant.
Books & Videosโ
- Sedgewick & Wayne, Algorithms, 4th ed., ยง2.4 โ "Priority Queues", with the heap developed from scratch.
Related Pagesโ
- Heapsort โ the sorting algorithm this structure exists inside.
- Shortest Paths โ Dijkstra's, where the priority queue determines the complexity.
- Scheduling โ priority queues in the OS.