3. Heaps and Heapsort
This chapter covers Lecture 2.2. A heap is a tree that always keeps its largest key at the top, and it fits neatly inside an array. Heapsort uses a heap to sort in time with extra memory.
The heap is also a priority queue. Dijkstra’s algorithm and Prim’s algorithm (Chapter 6) use a priority queue to pick the next vertex, so this chapter is needed there too.
1. What a Heap Is
A heap must satisfy two separate rules: a rule about its shape and a rule about its keys.
1.1 The shape rule: a heap structure
A binary tree of height is a heap structure if:
- every level is full, except possibly the bottom level (depth ), and
- the nodes on the bottom level are packed to the left, with no gaps.
In other words, you fill the tree level by level, left to right. This is also called an almost complete binary tree. Because of this shape, a heap with nodes has height . A full tree with levels holds nodes, so the height grows only as .
1.2 The key rule: the partial order tree property
A tree is a max-heap (the lecture says a maximising partial order tree) if every node’s key is greater than or equal to the keys of its children.
Two results follow:
- The largest key is at the root. Follow any path down from the root and the keys never increase.
- The rule says nothing about left against right. The left child can be larger or smaller than the right child. That is why it is only a partial order: a heap is not a sorted list.
A min-heap is the mirror image: every node is less than or equal to its children, so the smallest key is at the root.
1.3 Storing a heap in an array
Because the shape has no gaps, you can store the nodes level by level in an array, starting at index 1. No pointers are needed.
For the node at index :
| Relation | Index |
|---|---|
| Left child | |
| Right child | |
| Parent |
- Node is a leaf if .
- The last node with a child is at index . So nodes to are all leaves: about half the nodes.
- In array form, the key rule says: and , whenever those positions exist.
To check whether an array is a valid max-heap, check every parent against its children with these index rules. Example: . Node 4 holds 12, and its children (index 8 and 9) hold 7 and 10: fine. Node 5 holds 3, and its only child (index 10) holds 2: fine. Nodes 1 to 3 pass the same check, so this is a valid max-heap. A sorted-looking array is not needed: 10 sits after 7 and 12, but the heap rule only compares parent with child.
2. fixHeap: Repairing the Root
2.1 The situation
Suppose the root has an empty slot, and both subtrees below it are
already heaps. You have one key,
,
that must go somewhere in this tree. fixHeap finds the
right place.
2.2 The idea
The root must hold the largest key. The candidates are and the two children. The largest child is already the largest key in its own subtree.
- Compare the two children. Let the larger child be . (1 comparison.)
- Compare
with
.
(1 comparison.)
- If , put in the empty slot. Done.
- Otherwise, move up into the empty slot. Now ’s old place is empty. Repeat from there.
- If the empty slot reaches a leaf, put there.
So “sinks” down the tree, and at each level the larger child moves up to fill the gap.
2.3 The code (iterative)
fixHeap(H, k) { // H[1..currentSize] with the root empty
int j = 1, cj = 2; // j: the empty slot; cj: its left child
while (cj <= currentSize) {
if (cj < currentSize && H[cj] < H[cj+1])
cj++; // cj is now the larger child
if (k >= H[cj])
break; // k belongs in slot j
H[j] = H[cj]; // move the larger child up
j = cj; // the empty slot moves down
cj = 2 * j;
}
H[j] = k;
}
When the empty slot has only one child
(cj == currentSize), there is no sibling to compare with.
That level costs only 1 comparison.
2.4 Cost of fixHeap
Each level costs at most 2 comparisons, and the empty slot moves down one level each time. The height is . So:
The best case is 1 or 2 comparisons, when is already larger than both children: .
3. Building a Heap from an Unordered Array
3.1 The idea: fix from the bottom up
Start with the array in any order. It already has the heap shape, but not the key rule.
- A leaf is a heap of one node. So all the leaves are already heaps.
- Now take a node whose two subtrees are already heaps. Its own
subtree is a heap except possibly at its root. That is exactly the
situation
fixHeaphandles. - So fix the nodes from the bottom of the tree to the top. When you reach a node, its subtrees are already heaps.
The lecture writes this recursively as heapifying:
heapifying(H) {
if (H is not a leaf) {
heapifying(left subtree of H); // make the left subtree a heap
heapifying(right subtree of H); // make the right subtree a heap
k = root(H);
fixHeap(H, k); // then fix the root
}
}
This visits the subtrees before their root, which is a post-order traversal.
On an array, the same result comes from a simple loop. Run
fixHeap on the subtree rooted at each node, from the last
parent, index
,
back to index 1:
constructHeap(H, n) {
for (i = n / 2; i >= 1; i--)
fixHeap on the subtree rooted at i, with k = H[i];
}
Both versions fix every child subtree before its parent, so both produce the same heap.
3.2 Building a heap takes only linear time
You might expect
:
about
calls to fixHeap, each up to
.
That bound is true but not tight. Most nodes are near the bottom, and a
node near the bottom can sink only a short way.
- About nodes are leaves. They are never fixed.
- About nodes are one level above the leaves. Each sinks at most 1 level.
- About nodes can sink at most 2 levels, and so on.
- Only the root can sink the full levels.
In general, about nodes sit levels above the leaves. Each sinks at most levels. Each level of sinking costs at most 2 comparisons. So the total is at most
The sum is a constant: it equals 2. So the total is at most , and heap construction is .
The lecture states the same fact as a recurrence. Take a full tree with levels: nodes.
- Without the root, nodes remain, split evenly. So each subtree has nodes. The two calls cost .
fixHeapat the root sinks at most levels, at 2 comparisons each: .
This solves to . By the master method (Chapter 8): , , so . is polynomially smaller than . Case 1 gives .
3.3 The best case for construction
If the array is in decreasing order, every parent is
already larger than its children. Each fixHeap stops at
once:
- a node with two children: 2 comparisons (pick the larger child, then compare), no moves;
- a node with one child: 1 comparison.
There are parents. If is odd, all have two children: comparisons. If is even, one parent has a single child: . So the best case is exactly comparisons for construction. For example, 10 elements in decreasing order cost 9 comparisons.
4. Heapsort
4.1 The idea
- Build a max-heap from the array. The largest key is now at the root,
H[1]. - Take the root out and put it in the last position of the heap. That position now belongs to the sorted part. It is no longer in the heap.
- The last element of the heap used to be in that position. Re-insert
it with
fixHeap. The heap is now one element smaller. - Repeat until the heap is empty.
The heap shrinks from the right, and the sorted part grows from the right. Both live in the same array, so no extra memory is needed.
4.2 The code
heapSort(array, n) {
constructHeap(array, n);
for (i = n; i >= 1; i--) {
curMax = array[1]; // getMax: the root
k = array[i]; // the last element of the heap
array[i] = curMax; // the max goes to its final place
fixHeap(array[1..i-1], k); // re-insert k into the smaller heap
}
}
The middle three lines together are deleteMax: copy
the rightmost element of the lowest level to
,
delete that position, then fixHeap(H, k).
4.3 Running time
| Operation | Cost |
|---|---|
| getMax | |
| fixHeap | |
| deleteMax | (one fixHeap) |
| constructHeap / heapifying | |
| heapSort |
Heapsort costs to build, plus calls to deleteMax at each. Total: , about comparisons in the worst case.
The lecture gives
for the best, average and worst case. A sorted or a
reversed input does not help much: the element that
deleteMax re-inserts comes from the bottom of the heap, so
it is small and usually sinks most of the way down again. The heap
getting smaller does not change the order: the last
deletions still work on a heap of at least
elements, with height about
.
4.4 Space and stability
- Space:
extra memory. Heapsort is in-place. It needs no
recursion if
fixHeapis iterative. - Not stable. Example: is already a max-heap. The first deleteMax moves to the last position. Then moves to the root and is placed next. The result is : the two 2s have changed order.
5. Priority Queues
5.1 What a priority queue does
A priority queue stores a set of elements, each with a key called its priority. It supports:
insert(x): add element .Maximum(): return the element with the largest key.extractMax(): remove and return the element with the largest key.
A min-priority queue does the same with the smallest key. Priority queues are used in job scheduling, in simulation, and in Dijkstra’s and Prim’s algorithms.
5.2 A heap is a good priority queue
Maximum()is the root: .extractMax()is deleteMax: .insert(x): put in the next free position at the bottom (the end of the array). Then compare it with its parent, and swap while it is larger than its parent. It “bubbles up” at most the height of the tree: .- Decrease a key (in a min-heap, used by Dijkstra and Prim): change the key, then bubble the element up the same way: .
An unsorted array gives insert but extractMax. A sorted array gives extractMax but insert. The heap gives for both.
5.3 Applications
- The largest elements. Build a max-heap in , then call deleteMax times. Total . This beats sorting everything when is small.
- Merging
sorted lists with
elements in total, into one sorted list:
- Build a heap from the first element of each list: elements, .
- Remove the root and append it to the output. Remember which list it came from.
- Insert the next element of that same list into the heap (or use it
as the
in
fixHeap): . - Repeat until all elements are output.
6. The Sorting Algorithms So Far
| Algorithm | Best | Average | Worst | Extra space | Stable |
|---|---|---|---|---|---|
| Insertion sort | Yes | ||||
| Mergesort | buffer, or stack in place | Buffer version yes | |||
| Heapsort | No |
All entries are . Chapter 4 adds quicksort, counting sort and radix sort to this table.
7. Common Traps
- A heap is not sorted. Siblings can be in either order.
- Use 1-based indices: children of are and . With 0-based indices they would be and .
- fixHeap costs 2 comparisons per level: one to find the larger child, one to compare with .
- Building a heap is , not .
- A decreasing array is the best case for heap construction: exactly comparisons, no moves.
- Heapsort is in every case, uses extra memory, and is not stable.
- Heapsort needs no extra array for the sorted output: the sorted part grows from the right end of the same array.
- When you trace
fixHeap, the element that sinks is the last element of the heap, not the old root.