The problem it solves
That partial ordering is what makes a heap useful. It guarantees the smallest (or largest) element is always at the root, and it can be maintained in O(log n) per insertion or removal.
Why Partial Ordering Is Better Here
A BST fully sorts its data, which costs effort to maintain. A heap only guarantees the extreme value sits at the root. If all you ever ask is "what is the smallest item?", full sorting is wasted work.
That weaker promise makes the structure cheap to maintain: O(1) to read the root, O(log n) to insert or remove it. Note that heaps are terrible at searching for an arbitrary value — that is O(n), because the ordering gives you no guidance below the root.
An Array Pretending to Be a Tree
Heaps need no pointers at all. Because the tree is complete — every level full except possibly the last, filled left to right — positions can be computed with arithmetic:
Watch the tree and the array update together as you insert. They are the same structure drawn two ways. This makes heaps compact and cache-friendly — a real advantage over pointer-based trees.
Sift Up and Sift Down
- Insert — place the new value at the end of the array (the only spot that keeps the tree complete), then sift up: repeatedly swap with the parent while it is out of order. At most one swap per level, so O(log n).
- Extract root — take the root, move the last element into its place, then sift down: repeatedly swap with the smaller child until order is restored. Again O(log n).
The swap counter shows this: even with 15 elements, no operation needs more than about 4 swaps.
Try it yourself: Heaps and Priority Queues
- Insert a value smaller than the root. Watch it bubble all the way to the top — one swap per level, never more.
- Insert a large value. It stays near the bottom with no swaps at all — the average insert is far cheaper than the worst case.
The limitation: no efficient search or update
A heap answers "what is the smallest?" and nothing else. Finding an arbitrary element requires scanning — O(n) — because siblings are unordered.
That matters for algorithms that want to decrease a key: Dijkstra ideally updates a node's priority in place, and a binary heap cannot locate that node quickly. The standard workaround is lazy deletion — push a new entry and skip stale ones when popped — which is why Dijkstra implementations carry that check.
If you need both priority access and lookup, the usual answer is a heap plus a dictionary from item to its position, maintained together. That is more code and it is what a proper indexed priority queue provides.
| Need | Structure |
|---|
| Smallest or largest, repeatedly | Heap |
| Sorted iteration | Sorted list, or a balanced tree |
| Lookup by key | Hash table |
| Range queries | Balanced tree |
| Priority plus lookup | Heap plus an index dictionary |
A worked heap trace
Building a max-heap from [3, 9, 2, 1, 4, 5] by sifting down from the middle backwards makes the O(n) build concrete.
Indices 5, 4, 3 are leaves and need nothing. Start at index 2:
| Step | Node | Children | Action | Array |
|---|
| Start | — | — | — | [3, 9, 2, 1, 4, 5] |
| i = 2 | 2 | 5 (index 5) | Swap | [3, 9, 5, 1, 4, 2] |
| i = 1 | 9 | 1, 4 | Already largest | [3, 9, 5, 1, 4, 2] |
| i = 0 | 3 | 9, 5 | Swap with 9 | [9, 3, 5, 1, 4, 2] |
| i = 0 cont. | 3 | 1, 4 | Swap with 4 | [9, 4, 5, 1, 3, 2] |
Final heap: [9, 4, 5, 1, 3, 2]. The root is the maximum, every parent exceeds its children, and the siblings are unordered — 4 sits before 5, which is perfectly valid.
Extracting repeatedly then gives the sorted order: swap the root with the last element, shrink, sift down. That is heap sort.
Two things this trace demonstrates. The build genuinely does little work — three sift operations for six elements, most of them trivial. And the array's final layout looks unsorted, which is the point: a heap is not a sorted structure, only a partially ordered one.