How std::make_heap Builds In-Place Heaps in C++
The std::make_heap algorithm in C++ converts a standard
range into a binary max-heap directly within its existing memory buffer,
achieving optimal \(O(N)\) time
complexity. This article examines the mechanics behind this in-place
transformation, detailing how random-access ranges represent implicit
binary trees, how Floyd's heap construction algorithm works via
sift-down operations, and why this approach avoids heap allocations
while maintaining strict performance guarantees.
Implicit Binary Trees in Flat Memory
std::make_heap requires random-access iterators (such as
those provided by std::vector or raw arrays). It treats the
contiguous sequence as a complete binary tree without introducing any
pointer-based tree nodes.
For any element at zero-based index i:
- The left child is located at
2 * i + 1. - The right child is located at
2 * i + 2. - The parent is located at
(i - 1) / 2.
By relying on this mathematical indexing scheme,
std::make_heap restructures the elements in-place by
swapping values within the existing range, eliminating dynamic memory
allocation overhead.
Floyd’s Algorithm and the Sift-Down Operation
Rather than building the heap by inserting elements one-by-one—which
would take \(O(N \log N)\)
time—std::make_heap employs Floyd's linear-time heap
construction algorithm (often called bottom-up heapification).
The algorithm begins at the last non-leaf node in the sequence and iterates backward to the root (index 0). For an array of size \(n\), all elements from index \(n / 2\) to \(n - 1\) are leaves. Leaves are trivially valid heaps of size one, so the algorithm skips them and begins at index \((n / 2) - 1\).
At each step, the algorithm performs a sift-down (or
heapify-down) routine:
- Compare the value of the current parent node with its left and right children.
- If the parent satisfies the heap property (greater than or equal to both children in a max-heap), the sift-down terminates for that node.
- If one or both children violate the heap property, the parent is swapped with the larger child.
- The process repeats recursively or iteratively down the tree along that branch until the element reaches a valid position or becomes a leaf.
Mathematical Efficiency: Why the Process Is \(O(N)\)
A naive analysis might suggest that calling sift-down on \(N/2\) nodes yields \(O(N \log N)\) operations because tree height is \(\log N\). However, the height of most subtrees during bottom-up construction is small:
- Approximately half of all nodes are leaves (height 0), requiring 0 operations.
- One quarter of nodes are one level above the leaves (height 1), requiring at most 1 swap.
- One eighth of nodes are at height 2, requiring at most 2 swaps.
The total number of swaps is bounded by the summation:
\[\sum_{h=1}^{\log N} \frac{N}{2^{h+1}} \times h = O(N)\]
This structural distribution ensures that std::make_heap
performs at most \(3N\) comparisons and
far fewer moves, guaranteeing strictly linear \(O(N)\) execution time.
Custom Comparators and Stability
By default, std::make_heap uses
operator< to build a max-heap, positioning the largest
element at the root (index 0). An overloaded version accepts a custom
binary predicate, allowing developers to create min-heaps by passing
std::greater<T> or custom ordering rules.
Like most in-place heap implementations, std::make_heap
is not stable; relative orders of equivalent elements are not guaranteed
to be preserved during sift-down swaps. Once completed, the range
satisfies the binary heap invariant, enabling constant-time maximum
access at the front and logarithmic updates via
std::push_heap and std::pop_heap.