Loading learning content...
Here's a question that trips up many computer science students and programmers alike: How long does it take to build a heap from n unordered elements?
The naive answer goes like this: "Each insertion is O(log n), so n insertions must be O(n log n)." This reasoning is correct for repeated insertions—but there's a better algorithm.
The bottom-up heapify algorithm builds a heap in O(n) time. Not O(n log n). Just O(n).
This is one of the most elegant results in algorithm analysis, and understanding it deeply is a hallmark of algorithmic maturity. It demonstrates how careful analysis can reveal that an algorithm is faster than its components might suggest.
In this page, we'll prove this result rigorously, build intuition for why it works, and understand when to use heapify versus repeated insertion.
By the end of this page, you will understand: (1) Why building a heap via n insertions is O(n log n); (2) How the bottom-up heapify algorithm works; (3) The formal proof that heapify is O(n); (4) The mathematical insight behind the surprising result; (5) When to use heapify versus repeated insertion; (6) Practical implications for algorithm design.
Let's first establish a baseline by analyzing the obvious approach: insert each element one at a time.
Algorithm: Build Heap by Insertion
BUILD-HEAP-BY-INSERTION(array):
heap = empty heap
for each element x in array:
heap.insert(x)
return heap
Complexity Analysis:
Total cost:
T(n) = Σ log₂(k) for k from 1 to n
= log(1) + log(2) + log(3) + ... + log(n)
= log(n!)
≈ n log(n) - n + O(log n) [by Stirling's approximation]
= O(n log n)
| Heap Size | Sum of log(k) | O(n log n) Bound | Ratio |
|---|---|---|---|
| 10 | 15.1 | 33.2 | 0.45 |
| 100 | 364 | 664 | 0.55 |
| 1,000 | 8,530 | 9,965 | 0.86 |
| 10,000 | 118,458 | 132,877 | 0.89 |
| 1,000,000 | 18,488,885 | 19,931,568 | 0.93 |
The table shows that the sum of logarithms closely tracks n log n for large n. This confirms that the naive approach is genuinely O(n log n).
Why This Matters:
For n = 1 million elements:
That's nearly a 10× difference! For real-time systems or large datasets, this difference can be the deciding factor between an algorithm that works and one that fails.
Many developers unknowingly use the O(n log n) approach when the O(n) heapify is available. If you have all elements upfront, never build a heap by repeated insertion. Use the heapify algorithm instead.
The key insight of heapify is to build the heap from the bottom up rather than from the top down. Instead of inserting elements into a growing heap, we start with all elements in an array and "heapify" each subtree starting from the bottom.
Algorithm: Bottom-Up Heapify
BUILD-HEAP(array):
n = array.length
// Start from the last non-leaf node
// The last non-leaf is at index (n/2) - 1
for i from (n/2 - 1) down to 0:
BUBBLE-DOWN(array, i, n)
Why Start at (n/2 - 1)?
In a complete binary tree with n nodes:
We don't need to call bubble-down on leaves because there's nothing to bubble down to!
Visualization of the Algorithm:
Consider array: [4, 10, 3, 5, 1, 8, 7]
4 (index 0)
/ \
10 3 (indices 1, 2)
/ \ / \
5 1 8 7 (indices 3, 4, 5, 6)
n = 7, so we start at index (7/2) - 1 = 2.
Step 1: Bubble-down at index 2 (value 3):
[4, 10, 8, 5, 1, 3, 7]Step 2: Bubble-down at index 1 (value 10):
Step 3: Bubble-down at index 0 (value 4):
[10, 4, 8, 5, 1, 3, 7][10, 5, 8, 4, 1, 3, 7]Final max-heap: [10, 5, 8, 4, 1, 3, 7]
10
/ \
5 8
/ \ / \
4 1 3 7
When we bubble-down at index i, both subtrees rooted at 2i+1 and 2i+2 are already valid heaps (by the order of processing). So bubble-down only needs to find the correct place for element i among its descendants. The work done is proportional to the height of node i, not the height of the tree.
Now for the heart of the matter: proving that bottom-up heapify runs in O(n) time.
Setup:
Consider a complete binary tree with n nodes and height h = ⌊log₂(n)⌋.
Key Insight:
The cost of bubble-down for a node is proportional to its height (distance to its furthest leaf descendant), not its depth (distance to root).
Counting the Total Work:
At height k (measured from the bottom), there are at most ⌈n/2^(k+1)⌉ nodes.
For simplicity, let's use n/2^(k+1) (the exact analysis gives the same asymptotic result).
Total work:
T(n) = Σ (nodes at height k) × (cost for height k)
= Σ (n / 2^(k+1)) × k for k from 1 to h
= (n/2) × Σ (k / 2^k) for k from 1 to h
Evaluating the Sum:
The key is evaluating Σ k/2^k for k from 1 to ∞:
S = Σ k/2^k = 1/2 + 2/4 + 3/8 + 4/16 + ...
This is a well-known series. Let's derive it:
S = 1/2 + 2/4 + 3/8 + 4/16 + ...
2S = 1 + 2/2 + 3/4 + 4/8 + ...
Subtracting:
S = 2S - S = 1 + 1/2 + 1/4 + 1/8 + ... - (the adjustments cancel nicely)
Using the standard formula for this series:
Σ k × x^k = x / (1-x)² for |x| < 1
With x = 1/2:
Σ k / 2^k = (1/2) / (1 - 1/2)² = (1/2) / (1/4) = 2
Completing the Proof:
T(n) = (n/2) × Σ (k / 2^k)
≤ (n/2) × 2
= n
= O(n)
Therefore, heapify runs in O(n) time. □
Intuition Behind the Result:
The O(n) bound seems counterintuitive until you realize the distribution of work:
The work is heavily concentrated at the bottom of the tree, where most nodes are but where each node does little work. The few nodes near the top do more work per node, but there are so few of them that their contribution is bounded.
| Height | Nodes at Height | Work per Node | Total Work |
|---|---|---|---|
| 0 (leaves) | n/2 | 0 | 0 |
| 1 | n/4 | 1 | n/4 |
| 2 | n/8 | 2 | n/4 |
| 3 | n/16 | 3 | 3n/16 |
| k | n/2^(k+1) | k | kn/2^(k+1) |
| log n (root) | 1 | log n | log n |
The genius of bottom-up heapify is that it assigns the most work to the fewest nodes and the least work to the most nodes. This inverts the naive insertion approach, where early insertions (into small heaps) are cheap but later insertions (into large heaps) are expensive.
Let's directly compare the two approaches to understand the practical difference.
Approach 1: Build by Insertion (Top-Down)
Approach 2: Bottom-Up Heapify
Why the Difference?
The key asymmetry:
| Metric | Build by Insertion | Bottom-Up Heapify | Ratio |
|---|---|---|---|
| Total comparisons | ~20 million | ~2 million | 10:1 |
| Total swaps | ~10 million (avg) | ~1 million | 10:1 |
| Time complexity | O(n log n) | O(n) | O(log n) : O(1) |
| Approximate runtime* | ~50 ms | ~5 ms | 10:1 |
*Approximate runtimes for integer arrays on modern hardware.
When to Use Each Approach:
Use Bottom-Up Heapify when:
Use Repeated Insertion when:
The Takeaway:
If you have all n elements available, always use heapify. It's O(n) versus O(n log n)—a free 10× speedup for large inputs.
Python's heapq.heapify(list) uses the O(n) bottom-up algorithm. Java's PriorityQueue(Collection) constructor also uses O(n) heapify. But if you call add() n times, you get O(n log n). Know your library's constructors!
Let's prove O(n) using a different approach—counting swaps by where they end up rather than where they start.
Observation:
Each swap during heapify moves an element one level down. We can count the total swaps by counting how many times elements "fall" through each level.
Counting Down-Movements:
Consider any position in the tree at depth d (distance from root). During the entire heapify process:
Instead of tracking individual elements, let's count edge crossings:
Bounding Edge Crossings:
For each edge at depth d:
Wait, this gives O(n × h) = O(n log n)! We need a tighter analysis.
Tighter Analysis Using Potential:
Let's define a potential function and use amortized analysis.
Potential Definition: Φ(heap) = sum over all nodes i of: height(i)
For a complete binary tree:
Φ = Σ height(i) for all nodes
= Σ (h - depth(i))
= n×h - Σ depth(i)
The sum of depths in a complete binary tree is approximately n × h / 2 (half the nodes are leaves at max depth, etc.).
So Φ ≈ n × h / 2 = O(n log n) initially.
However, this approach needs refinement for heapify. Let's use a different counting.
Direct Counting by Level:
Each element at height k can fall at most k levels. There are roughly n/2^(k+1) elements at height k.
Total falls:
Σ (n/2^(k+1)) × k = (n/2) × Σ k/2^k = (n/2) × 2 = O(n)
This confirms our earlier result using a slightly different framing.
Having multiple proofs of the O(n) bound reinforces understanding. The "count by height" and "count by falls" approaches both reveal the same insight: the work is dominated by the many nodes near the bottom that don't travel far, not the few nodes near the top that travel far.
A natural question arises: if heapify is O(n), why is repeated insertion O(n log n)? Can't we somehow optimize insertion to get O(n)?
The Fundamental Difference:
Insertion (Bubble-Up):
Heapify (Bubble-Down):
The Mathematical Asymmetry:
In a complete binary tree:
This is because depths and heights are "opposite" views of the same tree, but the distribution matters.
Can We Use Bottom-Up for Insertion?
No, because insertion is inherently top-down from the perspective of the heap invariant:
The Constraint of Online Insertion:
Insertion is an "online" operation—each element must be positioned before the next arrives. We can't look ahead to see what's coming.
Heapify, by contrast, is "offline"—we have all elements and can process them in any order we choose. By choosing bottom-up order, we minimize total work.
Theoretical Lower Bound:
For comparison-based construction of a heap from n elements:
For insertion-based construction:
There's no trick to make repeated insertion O(n). The O(n log n) bound for insertion-based construction is tight. If you need to build a heap from a known collection, use heapify. If elements arrive one at a time, you're stuck with O(log n) per insertion.
Let's examine production-quality implementations of the heapify algorithm.
Python Implementation:
def heapify(arr: list) -> None:
"""
Transform arr into a max-heap in-place in O(n) time.
"""
n = len(arr)
# Start from the last non-leaf node and work backwards
# Last non-leaf is at index (n // 2) - 1
for i in range((n // 2) - 1, -1, -1):
_bubble_down(arr, i, n)
def _bubble_down(arr: list, index: int, heap_size: int) -> None:
"""
Bubble down element at index to restore heap property.
Assumes both subtrees are already valid heaps.
"""
while True:
largest = index
left = 2 * index + 1
right = 2 * index + 2
if left < heap_size and arr[left] > arr[largest]:
largest = left
if right < heap_size and arr[right] > arr[largest]:
largest = right
if largest != index:
arr[index], arr[largest] = arr[largest], arr[index]
index = largest
else:
break
# Usage:
data = [4, 10, 3, 5, 1, 8, 7]
heapify(data)
print(data) # [10, 5, 8, 4, 1, 3, 7]
JavaScript Implementation:
function heapify(arr) {
const n = arr.length;
// Start from last non-leaf and work backwards
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
bubbleDown(arr, i, n);
}
}
function bubbleDown(arr, index, heapSize) {
while (true) {
let largest = index;
const left = 2 * index + 1;
const right = 2 * index + 2;
if (left < heapSize && arr[left] > arr[largest]) {
largest = left;
}
if (right < heapSize && arr[right] > arr[largest]) {
largest = right;
}
if (largest !== index) {
[arr[index], arr[largest]] = [arr[largest], arr[index]];
index = largest;
} else {
break;
}
}
}
Key Implementation Notes:
(n/2) - 1 is the last non-leaf; earlier indices are also non-leaves(n/2) - 1 down to 0, ensuring children are heapified before parentsheap_size parameter: Important for heapsort, where we shrink the heap during extractionPython's heapq.heapify(list) implements exactly this algorithm. It transforms the list into a min-heap in-place in O(n) time. For max-heap behavior, you can negate values or use a custom wrapper class.
The O(n) heapify has important applications beyond simple heap construction.
Application 1: Heapsort
Heapsort consists of two phases:
The O(n) heap construction makes heapsort's total complexity O(n log n) with a good constant factor for the first phase.
Application 2: Finding the Kth Largest Element
One algorithm for finding the kth largest:
For k << n, this is O(n) dominated by the heap construction.
Alternatively:
This gives O(n log k), which is better when k is small.
Application 3: External Sorting / Merge Phase
When merging k sorted runs in external sorting:
The heap construction phase is O(k), not O(k log k).
Application 4: Median Maintenance
For finding the running median of a stream:
The O(n) initial construction enables efficient bootstrapping from existing data.
Application 5: Priority Queue Initialization
When creating a priority queue from known initial data:
new PriorityQueue<>(existingCollection) uses O(n) constructionheapq.heapify(list) transforms in-place in O(n)Application 6: Huffman Coding Initialization
Huffman coding for data compression:
For large alphabets, the O(k) construction matters.
Whenever you're building a heap from a known collection, the O(n) heapify gives you a "free" setup phase. The savings compound in algorithms where you need to rebuild heaps or construct from snapshots. Always reach for heapify over repeated insertion.
We've explored one of the most elegant results in algorithm analysis—the O(n) heap construction. Let's consolidate the key insights:
What's Next:
With insert (O(log n)), extract (O(log n)), and build (O(n)) analyzed, we have a complete picture of heap operation complexities. In the final page of this module, we'll synthesize these results to answer: Why are heaps the ideal data structure for priority queues? We'll compare heaps with alternatives and establish when heaps are the right choice.
You now understand one of algorithm analysis's most elegant results. The O(n) heapify bound isn't just a fact to memorize—you understand why it works, can prove it, and know when to apply it. This mathematical maturity distinguishes algorithm designers from algorithm users.