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 Θ(nlog⁡n)\Theta(n \log n) time with O(1)O(1) 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 hh is a heap structure if:

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 nn nodes has height ⌊lg⁡n⌋\lfloor \lg n \rfloor. A full tree with kk levels holds 2k−12^k - 1 nodes, so the height grows only as log⁡n\log n.

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:

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.

The same heap as a tree and as an array. The array lists the nodes level by level, left to right.

For the node at index kk:

Relation Index
Left child 2k2k
Right child 2k+12k + 1
Parent ⌊k/2⌋\lfloor k/2 \rfloor

To check whether an array is a valid max-heap, check every parent against its children with these index rules. Example: [20,14,9,12,3,8,5,7,10,2][20, 14, 9, 12, 3, 8, 5, 7, 10, 2]. 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, kk, 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 kk and the two children. The largest child is already the largest key in its own subtree.

  1. Compare the two children. Let the larger child be cc. (1 comparison.)
  2. Compare kk with cc. (1 comparison.)
    • If k≥ck \ge c, put kk in the empty slot. Done.
    • Otherwise, move cc up into the empty slot. Now cc’s old place is empty. Repeat from there.
  3. If the empty slot reaches a leaf, put kk there.

So kk “sinks” down the tree, and at each level the larger child moves up to fill the gap.

fixHeap. C must go back into the heap. R is larger than its sibling P and larger than C, so R moves up into the root, and C continues down into R’s old place.

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 ⌊lg⁡n⌋\lfloor \lg n \rfloor. So:

fixHeap≤2lg⁡n comparisons=O(log⁡n).\text{fixHeap} \le 2 \lg n \text{ comparisons} = O(\log n).

The best case is 1 or 2 comparisons, when kk is already larger than both children: O(1)O(1).


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.

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 ⌊n/2⌋\lfloor n/2 \rfloor, 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.

Heap construction on 1 2 3 4 5 6 7. The subtrees rooted at 2 and at 3 become heaps first.
The last step fixes the root. 1 sinks: 7 moves up, then 6 moves up. The final heap is 7 5 6 4 2 1 3.

3.2 Building a heap takes only linear time

You might expect O(nlog⁡n)O(n \log n): about n/2n/2 calls to fixHeap, each up to O(log⁡n)O(\log n). 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.

In general, about n/2h+1n/2^{h+1} nodes sit hh levels above the leaves. Each sinks at most hh levels. Each level of sinking costs at most 2 comparisons. So the total is at most

2(n4⋅1+n8⋅2+n16⋅3+…)=n(12+24+38+…)=n∑h≥1h2h.2\left(\frac{n}{4} \cdot 1 + \frac{n}{8} \cdot 2 + \frac{n}{16} \cdot 3 + \dots\right) = n \left(\frac{1}{2} + \frac{2}{4} + \frac{3}{8} + \dots\right) = n \sum_{h \ge 1} \frac{h}{2^h}.

The sum ∑h≥1h/2h\sum_{h \ge 1} h/2^h is a constant: it equals 2. So the total is at most 2n2n, and heap construction is O(n)O(n).

The lecture states the same fact as a recurrence. Take a full tree with dd levels: n=2d−1n = 2^d - 1 nodes.

W(n)=2W(n−12)+2lg⁡n.W(n) = 2W\left(\frac{n-1}{2}\right) + 2 \lg n.

This solves to O(n)O(n). By the master method (Chapter 8): a=2a = 2, b=2b = 2, so nlog⁡22=nn^{\log_2 2} = n. 2lg⁡n2 \lg n is polynomially smaller than nn. Case 1 gives Θ(n)\Theta(n).

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:

There are ⌊n/2⌋\lfloor n/2 \rfloor parents. If nn is odd, all have two children: 2⋅n−12=n−12 \cdot \frac{n-1}{2} = n - 1 comparisons. If nn is even, one parent has a single child: 2⋅n2−1=n−12 \cdot \frac{n}{2} - 1 = n - 1. So the best case is exactly n−1n - 1 comparisons for construction. For example, 10 elements in decreasing order cost 9 comparisons.


4. Heapsort

4.1 The idea

  1. Build a max-heap from the array. The largest key is now at the root, H[1].
  2. 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.
  3. The last element of the heap used to be in that position. Re-insert it with fixHeap. The heap is now one element smaller.
  4. 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 kk, delete that position, then fixHeap(H, k).

4.3 Running time

Operation Cost
getMax O(1)O(1)
fixHeap O(log⁡n)O(\log n)
deleteMax O(log⁡n)O(\log n) (one fixHeap)
constructHeap / heapifying O(n)O(n)
heapSort O(nlog⁡n)O(n \log n)

Heapsort costs O(n)O(n) to build, plus nn calls to deleteMax at O(log⁡n)O(\log n) each. Total: O(nlog⁡n)O(n \log n), about 2nlg⁡n2n \lg n comparisons in the worst case.

The lecture gives Θ(nlog⁡n)\Theta(n \log n) 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 n/2n/2 deletions still work on a heap of at least n/2n/2 elements, with height about lg⁡n−1\lg n - 1.

4.4 Space and stability


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:

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

An unsorted array gives O(1)O(1) insert but O(n)O(n) extractMax. A sorted array gives O(1)O(1) extractMax but O(n)O(n) insert. The heap gives O(log⁡n)O(\log n) for both.

5.3 Applications


6. The Sorting Algorithms So Far

Algorithm Best Average Worst Extra space Stable
Insertion sort nn n2n^2 n2n^2 O(1)O(1) Yes
Mergesort nlog⁡nn \log n nlog⁡nn \log n nlog⁡nn \log n O(n)O(n) buffer, or O(log⁡n)O(\log n) stack in place Buffer version yes
Heapsort nlog⁡nn \log n nlog⁡nn \log n nlog⁡nn \log n O(1)O(1) No

All entries are Θ(⋅)\Theta(\cdot). Chapter 4 adds quicksort, counting sort and radix sort to this table.


7. Common Traps