Algorithm Search⌘K

Proofs: Heapsort

These are the human proofs of Heapsort, at full length.

Heapsort sorts the array and keeps its items full

This proof shows: correctness, termination.

We write p(c)=⌊(c−1)/2⌋p(c) = \lfloor (c-1)/2 \rfloor for the parent of an index c≥1c \ge 1. The part A[0…m−1]A[0 \ldots m-1] is a heap when A[p(c)]≥A[c]A[p(c)] \ge A[c] for each cc with 1≤c<m1 \le c < m. The subtree of ii in a heap of size mm is the set of indices less than mm that have ii as an ancestor, together with ii. Each step of the algorithm is an exchange of two items, so the multiset of items of AA never changes. We prove only the order.

Lemma 1 (SIFT-DOWN). Let 0≤i<m0 \le i < m. Assume that each pair (p(c),c)(p(c), c) with cc in the subtree of ii and c≠ic \ne i satisfies A[p(c)]≥A[c]A[p(c)] \ge A[c], except the pairs whose parent is ii. Then SIFT-DOWN(A,i,m)(A, i, m) stops after at most ⌊lg⁡m⌋\lfloor \lg m \rfloor steps of its loop, and after it the subtree of ii satisfies the heap order. It changes only items of the subtree of ii.

Proof. Let i0i_0 be the value of ii at the call, and let xx be the item A[i0]A[i_0]. We use this invariant at the test of line L5, for the current index ii:

(S) Each pair (p(c),c)(p(c), c) with cc in the subtree of i0i_0 and c≠i0c \ne i_0 is in heap order, except perhaps the pairs whose parent is ii. If i≠i0i \ne i_0, then A[p(i)]A[p(i)] is at least each child of ii. The item at ii is xx.

Initialization. At the call i=i0i = i_0, so the first statement is the assumption, and the second statement does not apply.

Maintenance. Assume (S) and 2i+1<m2i + 1 < m. Lines L6 and L7 set cc to a child of ii with the largest item among the children of ii. If A[i]≥A[c]A[i] \ge A[c], line L8 returns, and A[i]A[i] is at least each child, so all pairs with parent ii are in order. Together with (S), the full subtree is in heap order. Otherwise A[c]>A[i]=xA[c] > A[i] = x. Line L9 exchanges them. Now the item at ii is the old A[c]A[c], which is at least the item of the other child of ii, by the choice of cc, and at least xx, the new item at cc. If i≠i0i \ne i_0, the old A[c]A[c] is at most A[p(i)]A[p(i)] by the second statement of (S), so the pair (p(i),i)(p(i), i) stays in order. The children of cc were at most the old A[c]A[c], because (c,child)(c, \text{child}) was in order by (S), so after line L10 sets i←ci \leftarrow c, the parent of the new ii holds the old A[c]A[c], which is at least each child of the new ii. Only the pairs with parent cc can now be out of order. So (S) holds for the new ii.

Termination. Each step moves ii to a child, so it increases the depth of ii by exactly 1. The depth of an index less than mm is at most ⌊lg⁡m⌋\lfloor \lg m \rfloor. So the loop runs at most ⌊lg⁡m⌋\lfloor \lg m \rfloor times. If it stops at line L5, then ii has no child less than mm, so there are no pairs with parent ii, and (S) gives the heap order of the whole subtree. If it stops at line L8, the maintenance step showed the heap order. Each exchange is between ii and a child of ii, both in the subtree of i0i_0. □\square

Lemma 2 (build phase). After line L1, A[0…n−1]A[0 \ldots n-1] is a heap.

Proof. We use the invariant: before the call for ii, the subtree of each index jj with i<j<ni < j < n is in heap order. At the start i=⌊n/2⌋−1i = \lfloor n/2 \rfloor - 1. Each j≥⌊n/2⌋j \ge \lfloor n/2 \rfloor has 2j+1≥n2j + 1 \ge n, so it has no child, and its subtree {j}\{j\} is in heap order. Before the call for ii, both children of ii are greater than ii, so their subtrees are in heap order. These are all the pairs of the subtree of ii whose parent is not ii, so Lemma 1 applies, and after the call the subtree of ii is in heap order. The call changes only the subtree of ii, so the subtree of each j>ij > i that is not inside the subtree of ii is unchanged. The subtrees of the indices j>ij > i inside the subtree of ii are parts of a subtree in heap order, so they are in heap order too. So the invariant holds for i−1i - 1. After the call for i=0i = 0, the subtree of 0, which is the whole array, is in heap order. If n≤1n \le 1 the loop does not run, and an array of at most one item is a heap. □\square

Theorem. HEAPSORT(A)(A) stops, and after it AA is sorted in non-decreasing order and holds the same multiset of items.

Proof. We use this invariant at the test of line L2, for the current value of endend:

(H) A[0…end]A[0 \ldots end] is a heap. A[end+1…n−1]A[end+1 \ldots n-1] is sorted in non-decreasing order. Each item of A[0…end]A[0 \ldots end] is at most each item of A[end+1…n−1]A[end+1 \ldots n-1].

Initialization. Before the first step, end=n−1end = n - 1. By Lemma 2, A[0…n−1]A[0 \ldots n-1] is a heap. The other two parts of (H) are about an empty range.

Maintenance. Assume (H) for end≥1end \ge 1. In a heap, each item is at most the item of its parent, so by induction on the depth each item is at most A[0]A[0]. So A[0]A[0] is a largest item of A[0…end]A[0 \ldots end]. Line L3 moves it to A[end]A[end]. It is at least each item of A[0…end−1]A[0 \ldots end-1], and by (H) at most each item of A[end+1…n−1]A[end+1 \ldots n-1]. So A[end…n−1]A[end \ldots n-1] is sorted, and each item of A[0…end−1]A[0 \ldots end-1] is at most each item of A[end…n−1]A[end \ldots n-1]. After the exchange, each pair of A[0…end−1]A[0 \ldots end-1] whose parent is not 0 is still in heap order, because only the item at index 0 changed. So Lemma 1 applies to SIFT-DOWN(A,0,end)(A, 0, end), and after line L4, A[0…end−1]A[0 \ldots end-1] is a heap. SIFT-DOWN changes only indices less than endend, so the sorted part is unchanged. Then the loop sets end←end−1end \leftarrow end - 1, and (H) holds.

Termination. The loop of line L2 runs n−1n - 1 times, and each call of SIFT-DOWN stops by Lemma 1. After the last step end=0end = 0, and (H) says that A[1…n−1]A[1 \ldots n-1] is sorted and that A[0]A[0] is at most each item of it. So A[0…n−1]A[0 \ldots n-1] is sorted. If n≤1n \le 1 the loop does not run, and the array is sorted. ■\blacksquare

Back to Heapsort