Skip to main content

Heapsort

Overviewโ€‹

Heapsort is selection sort with a better way of selecting. Selection sort scans the unsorted region to find its maximum in O(n); heapsort keeps that region as a heap so the maximum is at the root and extraction costs O(log n). That single substitution converts O(nยฒ) into O(n log n).

Its distinguishing property is the combination no other common sort offers: O(n log n) worst case in O(1) space. Mergesort needs a buffer; quicksort has a quadratic worst case; heapsort has neither problem.

Animation of heapsort: the bars are first rearranged into a heap, then the largest is repeatedly swapped to the end and the heap restored over the shrinking remainder
Two phases. First the array becomes a heap; then the root is repeatedly swapped to the end, shrinking the heap and growing the sorted tail. Wikimedia Commons, CC BY-SA 3.0

Core Conceptsโ€‹

PropertyValue
Best caseO(n log n)
AverageO(n log n)
Worst caseO(n log n) โ€” guaranteed
SpaceO(1) โ€” genuinely in place, no recursion
StableNo
AdaptiveNo โ€” sorted input costs the same as random

Architecture / Mechanismโ€‹

The array is used as both the heap and the output. The heap occupies a shrinking prefix; the sorted result grows as a suffix behind it.

def heapsort(a):
n = len(a)

# Phase 1: build a max-heap in place โ€” O(n), not O(n log n)
for i in range(n // 2 - 1, -1, -1):
sift_down(a, i, n)

# Phase 2: repeatedly move the max to the end and shrink the heap
for end in range(n - 1, 0, -1):
a[0], a[end] = a[end], a[0] # largest element to its final position
sift_down(a, 0, end) # restore the heap over the remaining prefix
return a

def sift_down(a, i, size):
while True:
largest = i
for child in (2 * i + 1, 2 * i + 2):
if child < size and a[child] > a[largest]:
largest = child
if largest == i:
return
a[i], a[largest] = a[largest], a[i]
i = largest

Phase 1 is O(n) โ€” see the heaps page for why building a heap is linear rather than n log n. Phase 2 does n extractions at O(log n) each, so it dominates: O(n log n) overall.

Using a max-heap rather than a min-heap is what makes the sort ascending: the largest element is swapped to the end, and each subsequent one lands just before it.

Practical Usageโ€‹

Heapsort is rarely the top-level choice, but it occupies two important roles:

  • The safety net in introsort. C++'s std::sort runs quicksort, and switches to heapsort when recursion exceeds 2ยทlogโ‚‚ n levels. This makes the worst case O(n log n) without giving up quicksort's speed on typical input. Heapsort is chosen for the fallback precisely because it needs no extra memory and has no bad case of its own.
  • Memory-constrained and real-time systems. Embedded and kernel contexts where an O(n) allocation is unacceptable and an O(nยฒ) tail is unacceptable. The Linux kernel's sort() is a heapsort.
# Partial sorting: the top k without sorting everything โ€” O(n + k log n)
import heapq
def top_k(items, k):
heap = list(items)
heapq.heapify(heap) # O(n)
return [heapq.heappop(heap) for _ in range(k)] # k ร— O(log n)

Stopping phase 2 after k extractions gives the k largest elements in O(n + k log n) โ€” better than a full sort when k is small, which is the same argument behind heapq.nlargest.

Edge Cases & Pitfallsโ€‹

Heapsort is the slowest O(n log n) sort in practice

Its complexity is excellent and its constant factor is not. sift_down jumps between indices i, 2i+1 and 2i+2 โ€” locations that grow exponentially far apart, so each level of a sift is a fresh cache miss. Quicksort's partition scans memory sequentially and mergesort's merges are also sequential; heapsort's access pattern is nearly the worst possible.

On typical in-memory arrays it commonly runs 2โ€“3ร— slower than quicksort despite identical asymptotic complexity. Choose it for its guarantees, not for its speed.

  • n // 2 - 1 is the last internal node. Starting the build loop anywhere else either wastes work on leaves or leaves part of the heap unbuilt.
  • sift_down must be bounded by size, not len(a) โ€” otherwise phase 2 sifts back into the already-sorted suffix and corrupts it.
  • It is not stable, and equal elements are reordered by the long-distance swaps.
  • A min-heap sorts descending. If you want ascending output, build a max-heap.

Comparisonsโ€‹

HeapsortQuicksortMergesortSelection
Worst caseO(n log n)O(nยฒ)O(n log n)O(nยฒ)
SpaceO(1)O(log n)O(n)O(1)
StableNoNoYesNo
LocalityPoorExcellentGoodGood
Typical speedSlowest of the threeFastestGoodVery slow
Choose whenGuarantees + no memoryDefault for arraysStability or external sortWrites are costly

Referencesโ€‹

  • Williams, J.W.J. (1964), "Algorithm 232: Heapsort", Communications of the ACM โ€” the original, which introduced the heap along with it.
  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 6 โ€” heapsort with the O(n) build-heap proof.
  • Linux kernel lib/sort.c โ€” a production heapsort, with comments on why it was chosen.

Books & Videosโ€‹