Structures

Building a heap from the bottom

Bottom-up heap construction is Θ(n) and repeated insertion is Θ(n log n), and the second of those is a worst case quoted as a behaviour. On random input, repeated insertion measures linear too — 2.22 comparisons per element against 1.87 — and the famous logarithmic factor never appears. On ascending input it appears in full, and it is a factor of six.

Turning an array into a heap has two standard methods and one standard comparison between them.

Repeated insertion. Take the elements one at a time and sift each up into the heap built so far. Each insertion costs at most the height, so the total is O(nlogn)O(n \log n).

Bottom-up, or Floyd’s method. Start at the middle of the array and sift each element down, working backwards to the front. Each sift costs at most the height of its subtree, and most subtrees are tiny — half the nodes are leaves and cost nothing, a quarter are one level up and cost one — so the total telescopes to O(n)O(n).

The comparison is presented as linear against linearithmic, and the natural conclusion is that repeated insertion is a logarithmic factor worse. At n=65,536n = 65{,}536 that factor would be sixteen.

Measured on random input, it is 1.21.

Building a heap two ways, random inputComparisons against n on logarithmic axes. Bottom-up construction fits n at 1.86 comparisons per element. Repeated insertion is quoted as n log n, and on this input the flattest class for it is n at a spread of 1.09; at n = 65,536 it costs 1.21 times the bottom-up count, against a log₂ n of 16. The famous logarithmic factor is a worst case and this is not the worst case.10³10⁴10³10⁴10⁵ncomparisonsbottom-up (Floyd)repeated insertionn from 128 to 65,536, random input, seeded1.21× between the two at the right-hand edge
Fig. 1 Both methods, comparisons against n on logarithmic axes. The two lines are parallel. Bottom-up construction fits n at 1.87 comparisons per element; repeated insertion also fits n, at 2.22. The vertical gap is a constant of 1.21 and it does not widen across three orders of magnitude, which is what the absence of a logarithmic factor looks like.

The measurement

Comparisons, on random input from a stated seed:

n bottom-up repeated insertion ratio
128 232 268 1.155
512 950 1,088 1.145
2,048 3,843 4,637 1.207
8,192 15,373 18,544 1.206
32,768 61,711 74,717 1.211
65,536 123,288 149,272 1.211

Bottom-up construction divided by nn is 1.813, 1.855, 1.876, 1.877, 1.883, 1.881 — flat to within 4% across a 512-fold range, at 1.87 comparisons per element. The linear class fits at a spread of 1.04.

Repeated insertion divided by nn is 2.09, 2.13, 2.26, 2.26, 2.28, 2.28. The linear class fits it at a spread of 1.09, and the linearithmic class fits at 2.10 and is refused.

Repeated insertion is linear on random input, measured across three orders of magnitude, and it is 21% more expensive than the method that is famous for being linear.

Why the worst case does not show up

The sift-up in an insertion walks from a leaf towards the root, stopping as soon as the parent is larger. The logn\log n in the bound is what happens when it never stops early — when the new element is larger than everything already in the heap and travels the whole way.

On random input, the new element is the largest so far with probability 1/i1/i at step ii, so it usually stops after a step or two. Summing the expected travel over all insertions gives a constant per element, and the constant is a small one. The worst case requires every element to be a new maximum, which means the input arrives in ascending order.

So the two methods are not linear-against-linearithmic. They are:

  • bottom-up: Θ(n)\Theta(n) always, on every input;
  • repeated insertion: Θ(n)\Theta(n) on random input, Θ(nlogn)\Theta(n\log n) on ascending input.

Which is the same shape as insertion sort, and it is the shape that a single headline class always hides. One of those algorithms has a class and the other has a class per input, and the summary reports the worst entry of the second as though it were the first kind of statement.

The input where the factor is real

On ascending input at n=16,384n = 16{,}384:

method comparisons swaps
bottom-up 32,740 16,371
repeated insertion 196,624 196,624

A factor of exactly 6.0 in comparisons and 12.0 in swaps. The insertion count is 12n12n, and log216,384=14\log_2 16{,}384 = 14 — every element travels almost the full height, because every element is a new maximum.

And on descending input both methods spend 16,383 comparisons and zero swaps, because a descending array already satisfies the max-heap property and both methods spend their time confirming it.

Three inputs, and the ratio between the two methods is 6.0, 1.21 and 1.00. There is no single number that describes how much worse repeated insertion is.

Building a heap two ways, sorted inputComparisons against n on logarithmic axes. Bottom-up construction fits n at 1.97 comparisons per element. Repeated insertion is quoted as n log n, and on this input the flattest class for it is nlogn at a spread of 1.18; at n = 16,384 it costs 6.01 times the bottom-up count, against a log₂ n of 14. This is the input the worst case is about, and here the factor is real.10³10⁴10³10⁴10⁵ncomparisonsbottom-up (Floyd)repeated insertionn from 128 to 16,384, sorted input, seeded6.01× between the two at the right-hand edge
Fig. 2 The same two methods on ascending input, which is what the n log n bound is about. Now the lines diverge — the upper one is bending away from the lower — and the factor at the right-hand edge is six. This is the picture the textbook comparison describes, and the previous figure is the picture of what usually happens.

Why bottom-up is linear, in one paragraph

The telescoping argument is worth having in full, because it is short and because the intuition that “most of the work is at the top” gets it backwards — which is the same mistake the merge-sort excess makes.

A heap of nn nodes has about n/2n/2 leaves at height 0, n/4n/4 at height 1, n/8n/8 at height 2, and so on. Sifting a node of height hh costs at most hh swaps and 2h2h comparisons. So the total is bounded by

h0n2h+1h  =  nh0h2h+1  =  n\sum_{h \ge 0} \frac{n}{2^{h+1}} \cdot h \;=\; n \sum_{h\ge 0} \frac{h}{2^{h+1}} \;=\; n

because h/2h+1\sum h/2^{h+1} converges to 1. The expensive nodes are rare in exactly the proportion needed to keep the sum finite: there is one node of height logn\log n and it costs logn\log n, which is a vanishing share of a linear total.

Measured, the swap count is 1.87 comparisons and 0.74 swaps per element at n=65,536n = 65{,}536 — against the bound’s 2 comparisons per element. The bound is tight to within 7%, which is unusual for a bound derived by a series of upper estimates.

What the choice actually costs

Given a factor of 1.21 on random input, the practical case for bottom-up construction is weaker than the classes suggest and it is not zero, and it is worth separating the three reasons.

The worst case is real and is a common input. Ascending order is not a contrived adversarial arrangement; it is the shape of any sequence that arrives sorted, which is most of them. Bottom-up construction is 6 times cheaper there and identical on random input to within 21%, so it is never worse and sometimes much better.

Only one of them can be done at all in some situations. Bottom-up construction needs every element in hand before it starts. A priority queue that receives elements over time has no choice but repeated insertion, and the comparison does not arise. This is why heapsort uses the bottom-up method — the array is right there — and why Dijkstra’s queue uses insertions.

The measured gap is not stable either. The ratio runs 1.155, 1.145, 1.207, 1.206, 1.211, 1.211 across the sweep — rising for the first few doublings and then settling. A constant that has only just stopped moving at n=32,768n = 32{,}768 is a constant quoted at the reader’s risk, and it is the drift the constants field is about in miniature: the number is real, it is reproducible, and it is a number for a range rather than for the algorithm.

Their swap counts differ more than their comparison counts. On random input at n=65,536n = 65{,}536, bottom-up does 48,702 swaps and insertion does 83,748 — a factor of 1.72 against the comparison factor of 1.21. Data movement is the count that usually decides when the elements are anything larger than an integer, and it is the count that most favours the bottom-up method.

None of those is “it is a logarithmic factor faster”, which is the reason usually given.

Where each algorithm looks, and whenEvery array access from one run of each algorithm on 256 random elements: time along the horizontal axis, array index up the vertical. Heapsort makes 15% of its accesses to the next element or the same one; Merge sort makes 49%. That difference is invisible in the comparison count and is most of what the machine feels.Heapsort15% sequential · 14,044 accesses2560Merge sort49% sequential · 7,540 accesses2560time (accesses, left to right) · index (bottom to top)one run each, n = 256every access plotted
Fig. 3 Heapsort’s access pattern, beside merge sort’s. The dense band at the left of the heapsort trace is the construction phase this essay is about — bottom-up, and over in a linear number of operations. Everything after it is the extraction phase, which is where heapsort’s n log n and its poor locality both live.
Building a heap two ways, random inputComparisons against n on logarithmic axes. Bottom-up construction fits n at 1.86 comparisons per element. Repeated insertion is quoted as n log n, and on this input the flattest class for it is n at a spread of 1.09; at n = 8,192 it costs 1.21 times the bottom-up count, against a log₂ n of 13. The famous logarithmic factor is a worst case and this is not the worst case.10³10³10⁴ncomparisonsbottom-up (Floyd)repeated insertionn from 128 to 8,192, random input, seeded1.21× between the two at the right-hand edge
Fig. 4 The same two methods over a shorter range. The ratio at the right-hand edge is 1.21 here and 1.21 at n = 65,536, which is the sense in which the gap is a constant — and the reason a figure drawn to 8,192 and one drawn to 65,536 tell the same story is the reason the fit grants linear to both.

Where heapsort’s cost actually is

Heapsort is construction followed by extraction: build the heap, then repeatedly swap the root to the end and sift the new root down over a shrinking heap. The construction is the linear phase this essay is about. Splitting one run into its two phases:

n build extract build’s share build ÷ n extract ÷ n log₂ n
1,024 1,891 15,425 10.9% 1.85 1.506
4,096 7,712 77,961 9.0% 1.88 1.586
65,536 123,288 1,771,626 6.5% 1.88 1.690

Construction is under a tenth of the algorithm and its share is falling — linear against linearithmic, exactly as the classes predict, which is one of the rare places on this site where the classes describe the behaviour without a caveat.

Three consequences follow, and the first is the practical one.

Optimising the linear phase is optimising 6.5% of the algorithm. The difference between the two construction methods on random input is 21% of a phase that is 6.5% of the total, which is 1.4% of heapsort. The choice between them is worth making because bottom-up is not harder to write, and it is not worth thinking about for more than a minute.

The extraction phase is where the interesting inefficiency is. Each extraction sifts an element taken from the bottom of the heap all the way down again, and that element is almost always small, so the sift almost always travels the full height. The measured extract ÷ nlog2nn\log_2 n constant is 1.69 at n=65,536n = 65{,}536 against merge sort’s 0.838 — heapsort compares about twice as much as merge sort, and essentially all of the excess is here.

There is a well-known variant that addresses exactly this. Bottom-up heapsort — Wegener’s, confusingly named, and unrelated to Floyd’s construction — observes that the sifted element usually ends near the bottom, so it walks the path of larger children to the leaf first, then searches back upwards for the insertion point. That takes the comparisons per extraction from about 2logn2\log n to about logn\log n, and it is the change that would matter. This site does not implement it, so no number for it is quoted here.

The two phases have opposite locality. Construction sifts small subtrees, which are contiguous stretches of the array; extraction sifts from the root through the whole array. Heapsort’s poor access pattern is an extraction-phase property, and the linear phase this essay is about is the well-behaved part in both resources at once.

What an average-case bound would have said

The gap between Θ(nlogn)\Theta(n \log n) and the measured 2.22n2.22n is not a failure of the analysis. It is the difference between a worst-case bound and an average-case one, and the average-case one is derivable in three lines — which makes its absence from every account of this algorithm the interesting part.

Insert element ii into a heap of i1i-1 elements. It travels to the root only if it is the largest of the ii, which for a random arrival order happens with probability 1/i1/i. It travels at least kk levels only if it beats the 2k12^k - 1 elements on the path above it, which happens with probability about 2k2^{-k}. So the expected travel is

k12k=1\sum_{k \ge 1} 2^{-k} = 1

levels, independent of ii and independent of nn. Summed over nn insertions: a constant per element, and the constant is small.

That derivation is not hard, it is not new, and it produces a better answer than the one everybody quotes — an exact class rather than an upper bound that overstates by a factor of sixteen. The measured 2.22 comparisons per element is two comparisons per level travelled plus the one that stops the walk, which is what the derivation predicts.

Two questions follow and only one of them has a satisfying answer.

Why is the worst case the one taught? Because it is the one that composes. A bound that holds on every input can be used inside another analysis without checking anything; an average-case bound carries a distributional assumption that has to be discharged every time it is used. That is a real reason and it is why worst-case analysis is the default in the field.

Why is the average case not taught beside it? No good reason. It is three lines, it is exact, it explains the behaviour anybody who measures will observe, and its absence leaves the impression that a structure is a logarithmic factor worse than it is. This is the same complaint the constants field makes: the missing number is not hard to obtain, and the habit of not obtaining it is the whole of the problem.

Both readings hold at other ranges, which is the check that the two lines are classes rather than coincidences of where the sweep stopped.

Building a heap two ways, sorted inputComparisons against n on logarithmic axes. Bottom-up construction fits n at 1.98 comparisons per element. Repeated insertion is quoted as n log n, and on this input the flattest class for it is nlogn at a spread of 1.21; at n = 65,536 it costs 7.00 times the bottom-up count, against a log₂ n of 16. This is the input the worst case is about, and here the factor is real.10³10⁴10³10⁴10⁵10⁶ncomparisonsbottom-up (Floyd)repeated insertionn from 128 to 65,536, sorted input, seeded7.00× between the two at the right-hand edge
Fig. 5 Ascending input taken to sixty-five thousand rather than sixteen. Bottom-up construction still fits nn at 1.98 comparisons an element; repeated insertion’s flattest class is still nlognn\log n, at a spread of 1.21, and the ratio at the right-hand edge has grown from 4.7 to 7.00 against a log2n\log_2 n of 16. The factor is real here and it is still not the logarithm.
Building a heap two ways, random inputComparisons against n on logarithmic axes. Bottom-up construction fits n at 1.87 comparisons per element. Repeated insertion is quoted as n log n, and on this input the flattest class for it is n at a spread of 1.07; at n = 8,192 it costs 1.21 times the bottom-up count, against a log₂ n of 13. The famous logarithmic factor is a worst case and this is not the worst case.10³10³10⁴ncomparisonsbottom-up (Floyd)repeated insertionn from 512 to 8,192, random input, seeded1.21× between the two at the right-hand edge
Fig. 6 And random input over a shorter range, where the same fit is asked of a sixteenth as many points. Bottom-up fits nn at 1.87; repeated insertion’s flattest class is nn at a spread of 1.07, and the ratio at n = 8,192 is 1.21 against a log2n\log_2 n of 13. On this input the logarithmic factor does not appear at any size drawn.

Which method a priority queue can use

There is one setting where the comparison in this essay does not arise, and it is the commonest use of a heap.

A priority queue receives its elements over time. There is no array to sift down over, so bottom-up construction is unavailable and repeated insertion is the only option. Dijkstra’s algorithm pushes VV times, pops VV times and decrease-keys somewhere between zero and EE times, and none of those is a heapify.

Which means the linear-time construction, elegant as it is, applies to exactly one situation: an array that is already in hand and is about to be consumed as a heap. That is heapsort, and it is heapsort’s first phase, and — as the phase split above shows — it is 6.5% of heapsort at n=65,536n = 65{,}536.

The result is real, the derivation is beautiful, and its applicability is narrower than its fame. Worth saying, because a linear-time algorithm for something that sounds like it should cost nlognn\log n is exactly the kind of result that gets remembered without its scope.

Comparisons and swaps at n = 512Selection sort performs the most comparisons of any algorithm here and among the fewest swaps — it never moves an element it does not have to. Ordering these algorithms by comparisons and ordering them by swaps gives two different orders, which is why the question "how many operations" needs the operation named before it has an answer.comparisonsswapsInsertion sort63,071 / 0Selection sort130,816 / 504Bubble sort129,688 / 62,563Merge sort3,964 / 0Heapsort7,653 / 4,170Quicksort5,049 / 2,380n = 512, random inputcounted in the same run
Fig. 7 Comparisons against swaps for the sorting algorithms, including the heapsort this essay’s construction phase belongs to. Bottom-up construction’s advantage over repeated insertion is larger in swaps than in comparisons — 1.72 against 1.21 at n = 65,536 — and this is the axis on which that difference would show.

Why one walk stops early and the other does not

Two of this essay’s constants look unrelated and have one cause, and stating it explains both at once.

A sift-up stops almost immediately. The derivation above gives an expected travel of one level, because the inserted element has to beat everything on the path to the root and the chance of beating 2k12^k-1 elements falls like 2k2^{-k}.

A sift-down travels almost the whole way. Bottom-up construction measures 1.87 comparisons per element against a bound of 2 — tight to 7% — which means the descents are going nearly as far as the bound allows.

Both follow from the same fact about a heap’s shape: most of the nodes are near the bottom. A subtree of height hh has half its nodes in the bottom level and three quarters in the bottom two. So an element being sifted down into a random subtree is being placed among nodes that are mostly deep, and it usually belongs deep — while an element being sifted up is being compared against nodes that are mostly shallow relative to it, and it usually loses immediately.

That is one asymmetry producing two constants in opposite directions, and it is why the bound is loose in one case and tight in the other. Repeated insertion’s O(nlogn)O(n\log n) overstates by a factor of sixteen because the walk almost never goes far; bottom-up’s 2n2n overstates by 7% because the walk almost always does.

It also settles which of the two bounds is worth quoting. A bound whose slack is 7% is a description; a bound whose slack is a factor of sixteen is a permission. The two are written in the same notation and the difference between them is not visible in it — which is the constants field’s standing complaint, arriving here as a statement about bounds rather than about classes.

The branching factor, and which phase chooses it

The phase split above says construction is 6.5% of heapsort and extraction is the rest, and there is a parameter that trades between them which the essay has held fixed at two.

A dd-ary heap gives each node dd children instead of two. Its height is logdn\log_d n, so:

A sift-down costs more per level and there are fewer levels. Each level compares dd children to find the largest and then one more against the element, so the cost is about dlogdn=dlog2n/log2dd\log_d n = d\log_2 n / \log_2 d — which is minimised at d=2d = 2 and rises from there. Extraction, which is all sift-downs, wants a binary heap.

A sift-up costs one comparison per level and there are fewer levels. So its cost is logdn\log_d n, which falls as dd grows. Insertion and decrease-key want a wide heap.

And construction stays linear at every dd, by the same telescoping argument with a different ratio.

So the parameter is decided by which operation dominates. Heapsort is extraction-dominated and takes d=2d = 2. Dijkstra’s algorithm performs up to EE decrease-keys against VV pops, so on a sparse graph the sift-ups outnumber the sift-downs and a four-ary heap is the common recommendation — a small improvement to the term that dominates, bought at almost no cost, which is exactly why it survives where the asymptotically better structures do not.

The same structure, two workloads, two different right answers for a number that appears in no complexity class. That is this collection’s most repeated finding and it is worth noting that it arrives here as a consequence of the phase split rather than as a separate observation: knowing which phase dominates is what makes the parameter decidable at all.

The general form

This essay is the third case on the site of a declared class being refused by measurement, and it is the mildest and the most typical of the three.

Bubble sort on nearly sorted input fits neither of its candidate classes. The hybrid on reversed input fits different classes over different ranges. Here, repeated insertion fits nn on the input everybody uses and nlognn\log n on the input the bound is about, and both fits are clean.

What all three have in common is that the published class is correct and describes a case. What differs is how far the case is from the common one, and this is the one where the distance is largest: a factor of sixteen predicted, a factor of 1.21 observed.

The reason to keep saying this is that the failure is systematic rather than occasional. A worst-case bound is the maximum over a set, the maximum is attained somewhere, and where it is attained is a fact about the input that the notation is designed to discard. Every time this site measures a worst-case bound against a typical input, the two are different, and the size of the difference is not predictable from the bound.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

HeapHeapifyPriority queueSift-downSwapsWorst case