How can std::make_heap be implemented while making at most 3N comparisons?

A binary heap over n elements can be created in O(n) time using a clever algorithm and a clever analysis. In what follows I’m just going to talk about how this works assuming that you have explicit nodes and explicit left and right child pointers, but this analysis is still perfectly valid once you compress it into an array.

The algorithm works as follows. Start off by taking about half of the nodes and treating them as singleton max-heaps – since there’s only one element, the tree containing just that element must automatically be a max-heap. Now, take these trees and pair them off with one another. For each pair of trees, take one of the values that you haven’t used yet and execute the following algorithm:

  1. Make the new node the root of the heap, having its left and right child pointers refer to the two max-heaps.

  2. While this node has a child that’s larger than it, swap the child with its larger child.

My claim is that this procedure ends up producing a new max heap containing the elements of the two input max-heaps, and it does so in time O(h), where h is the height of the two heaps. The proof is an induction on the height of the heaps. As a base case, if the subheaps have size zero, then the algorithm terminates immediately with a singleton max-heap, and it does so in O(1) time. For the inductive step, assume that for some h, this procedure works on any subheaps of size h and consider what happens when you execute it on two heaps of size h + 1. When we add a new root to join together two subtrees of size h + 1, there are three possibilities:

  1. The new root is larger than the roots of both subtrees. Then in this case we have a new max-heap, since the root is larger than any of the nodes in either subtree (by transitivity)

  2. The new root is larger than one child and smaller than the other. Then we swap the root with the larger subchild and recursively execute this procedure again, using the old root and the child’s two subtrees, each of which are of height h. By the inductive hypothesis, this means that the subtree we swapped into is now a max-heap. Thus the overall heap is a max-heap, since the new root is larger than everything in the subtree we swapped with (since it’s larger than the node we added and was already larger than everything in that subtree), and it’s also larger than everything in the other subtree (since it’s larger than the root and the root was larger than everything in the other subtree).

  3. The new root is smaller than both its children. Then using a slightly modified version of the above analysis, we can show that the resulting tree is indeed a heap.

Moreover, since at each step the heights of the child heaps decreases by one, the overall runtime for this algorithm must be O(h).


At this point, we have a simple algorithm for making a heap:

  1. Take about half the nodes and create singleton heaps. (You can compute explicitly how many nodes will be needed here, but it’s about half).
  2. Pair those heaps off, then merge them together by using one of the unused nodes and the above procedure.
  3. Repeat step 2 until a single heap remains.

Since at each step we know that the heaps we have so far are valid max-heaps, eventually this produces a valid overall max-heap. If we’re clever with how we pick how many singleton heaps to make, this will end up creating a complete binary tree as well.

However, it seems like this should run in O(n lg n) time, since we do O(n) merges, each of which runs in O(h), and in the worst case the height of the trees we’re merging is O(lg n). But this bound is not tight and we can do a lot better by being more precise with the analysis.

In particular, let’s think about how deep all the trees we merge are. About half the heaps have depth zero, then half of what’s left has depth one, then half of what’s left has depth two, etc. If we sum this up, we get the sum

0 * n/2 + 1 * n/4 + 2 * n/8 + … + nk/(2k) = Σk = 0⌈log n⌉ (nk / 2k) = n Σk = 0⌈log n⌉ (k / 2k+1)

This upper-bounds the number of swaps made. Each swap requires at most two comparisons. Therefore, if we multiply the above sum by two, we get the following summation, which upper-bounds the number of swaps made:

n Σk = 0 (k / 2k)

The summation here is the summation 0 / 20 + 1 / 21 + 2 / 22 + 3 / 23 + … . This is a famous summation that can be evaluated in multiple different ways. One way to evaluate this is given in these lecture slides, slides 45-47. It ends up coming out to exactly 2n, which means that the number of comparisons that end up getting made is certainly bounded from above by 3n.

Hope this helps!

Leave a Comment