Part II · Sorting Chapter 8

Sorting in Linear Time

Why Ω(n lg n) is a hard floor for comparison sorting, and three algorithms that get underneath it by refusing to compare.

Three sorts so far, all Θ(n lg n), none better. Chapter 8 proves that is no accident: any algorithm that determines the sorted order using only comparisons needs Ω(n lg n) comparisons in the worst case. The proof is a counting argument over decision trees and it is short, general, and complete — it rules out not just the algorithms we have seen but every algorithm of that kind that will ever be written. The rest of the chapter escapes the bound by making assumptions about the keys themselves: counting sort, radix sort, and bucket sort all run in linear time by extracting information from keys without comparing them.

4th edition note. Essentially unchanged. Procedures pass sizes explicitly, and the counting-sort presentation is slightly clarified around why the final loop runs downward.

Contents

  1. The comparison-sort model
  2. Decision trees
  3. The Ω(n lg n) lower bound
  4. Counting sort
  5. Stability, and why the last loop runs backwards
  6. Radix sort
  7. Bucket sort
  8. Choosing a sort
  9. Recap

The comparison-sort model

A comparison sort is one that gains order information about the input only by comparing pairs of elements: tests of the form aᵢ < aⱼ, aᵢ ≤ aⱼ, aᵢ = aⱼ, aᵢ ≥ aⱼ, or aᵢ > aⱼ. It may not inspect the values in any other way.

Insertion sort, merge sort, heapsort, and quicksort are all comparison sorts. So is every general-purpose sort() that takes a comparator function — the comparator is literally the only window it has onto the data.

Two simplifying observations make the analysis clean. First, we may assume all elements are distinct, since ties only make sorting easier and we are after a lower bound. Second, given distinctness, all five comparison forms are equivalent in the information they yield, so we may assume every comparison is of the form aᵢ ≤ aⱼ.

Decision trees

Abstract away everything except the comparisons. A decision tree is a full binary tree representing the comparisons performed by a particular sorting algorithm on inputs of a given size.

1:2 2:3 1:3 1:3 2:3 1,2,3 1,3,2 3,1,2 2,1,3 2,3,1 3,2,1 > > 3! = 6 leaves, one per possible answer — height 3, so 3 comparisons in the worst case
Figure 8.1 — A decision tree for sorting three elements. Every execution of the algorithm is one root-to-leaf path, and its length is the number of comparisons made.

Two facts follow immediately from this correspondence:

The Ω(n lg n) lower bound

Theorem 8.1. Any comparison sort algorithm requires Ω(n lg n) comparisons in the worst case.

The proof is three lines of counting.

Step 1: the tree needs at least n! leaves. A correct sorting algorithm must be able to produce every one of the n! permutations of its input, since any one of them could be the right answer. Each permutation must appear as at least one reachable leaf. So the number of reachable leaves is at least n!.

Step 2: a binary tree of height h has at most 2ᵗ leaves. Elementary.

Step 3: combine and take logarithms.

n! ≤ 2h h ≥ lg(n!) = Θ(n lg n) // Stirling's approximation, Chapter 3 h = Ω(n lg n)

The step lg(n!) = Θ(n lg n) is the one piece of real mathematics, and Chapter 3 already supplied it from Stirling’s approximation. Intuitively: n! has n factors, most of them larger than n/2, so n! is at least (n/2)n/2 and its logarithm is at least (n/2)lg(n/2) = Ω(n lg n).

Corollary 8.2. Heapsort and merge sort are asymptotically optimal comparison sorts: their O(n lg n) upper bounds match the Ω(n lg n) lower bound to within a constant factor.
Why this proof is worth admiring. It says nothing about any particular algorithm. It bounds every possible comparison sort, including ones nobody has invented, by counting how much information a yes/no question can carry. Each comparison yields one bit; you need lg(n!) bits to identify one permutation out of n!; therefore you need lg(n!) comparisons. Information-theoretic arguments of this shape recur throughout complexity theory.
What the bound does not say. It bounds comparisons, not running time, and it bounds only comparison sorts. It says nothing about algorithms that look at the keys some other way — indexing an array by a key value, examining individual digits, or hashing. That loophole is exactly what the rest of the chapter walks through.

Counting sort

Assumption: every input element is an integer in the range 0 to k, for some known k.

The idea: for each input value x, count how many elements are less than or equal to x. That count is the final position of x. No comparisons required — just array indexing.

COUNTING-SORT(A, B, n, k) 1 let C[0:k] be a new array 2 for i = 0 to k 3 C[i] = 0 4 for j = 1 to n 5 C[A[j]] = C[A[j]] + 1 6 // C[i] now holds the number of elements equal to i 7 for i = 1 to k 8 C[i] = C[i] + C[i-1] 9 // C[i] now holds the number of elements less than or equal to i 10 for j = n downto 1 // backwards, for stability — see below 11 B[C[A[j]]] = A[j] 12 C[A[j]] = C[A[j]] - 1

Four loops: initialise, tally, accumulate a running total, then place. Output goes to a separate array B, so this is not in place.

input A 2 5 3 0 2 3 0 3 C after tally 2 0 2 3 0 1 “how many equal to i” C cumulative 2 2 4 7 7 8 “how many ≤ i” = last output slot for i 012 345 output B 0 0 2 2 3 3 3 5
Figure 8.2 — Counting sort with k = 5. The cumulative array says “there are 7 elements ≤ 3”, so the last 3 goes to slot 7. Decrement and the next 3 goes to slot 6.

Running time. The loops cost Θ(k), Θ(n), Θ(k), Θ(n), so the total is Θ(n + k). When k = O(n) this is Θ(n) — genuinely linear.

The bound has teeth. Counting sort is linear only when k = O(n). Sorting a million 32-bit integers with counting sort means k = 2³², so Θ(n + k) is four billion operations and a 16 GB count array to sort a million values. Use it when the key range is small and known: ages, bytes, grades, day-of-year, priority levels.

Stability, and why the last loop runs backwards

Counting sort is stable: elements with equal keys appear in the output in the same order they appeared in the input. That property is not incidental, and it is the entire reason line 10 counts downward.

Walk it through. C[3] = 7 means seven elements are ≤ 3, so the last 3 in the input belongs in slot 7. Scanning the input from the right, the first 3 encountered is the last one, and it correctly claims slot 7. Decrementing C[3] to 6 means the next 3 encountered — which came earlier in the input — claims slot 6. Earlier input elements get earlier output slots. Stability preserved.

Reverse line 10 to for j = 1 to n and the algorithm still sorts correctly, but equal elements come out in reverse input order. That breaks radix sort, which depends on the stability of its digit sort. This is the one-character change that quietly destroys the next algorithm in the chapter.

Radix sort

Counting sort needs a small key range. Radix sort removes that limitation by sorting d-digit numbers one digit at a time, using a stable sort on each digit.

RADIX-SORT(A, n, d) 1 for i = 1 to d 2 use a stable sort to sort array A[1:n] on digit i

Digit 1 is the least significant. Counter-intuitively, you sort from the least significant digit upward, not the most significant.

input after ones digit after tens digit after hundreds 329457 657839 436720 355 720 355 436 457 657 329 839 720 329 436 839 355 457 657 329 355 436 457 657 720 839
Figure 8.3 — Radix sort, least significant digit first. Look at 720 and 329 in the middle column: they tie on the tens digit, and stability keeps 720 before 329 because the previous pass left it there.
Why least significant first, and why stability is mandatory. By induction on i: after sorting on digit i, the array is sorted on the low-order i digits. When the pass on digit i+1 runs, elements that differ on that digit are ordered correctly by the sort; elements that tie on it keep their previous relative order, which by the inductive hypothesis was correct on the lower digits. Remove stability and every tie scrambles the work of all previous passes, and the algorithm is simply wrong.

Running time. With d digits each in range 0 to k, using counting sort per pass:

T(n) = Θ( d·(n + k) ) // Θ(n) when d is constant and k = O(n)

The chapter also asks how to choose the digit size. Given n numbers of b bits each, split each into b/r digits of r bits, so k = 2ᵣ - 1:

T(n, b) = Θ( (b/r)·(n + 2ᵣ) )

Choosing r ≈ lg n balances the two terms and gives Θ(bn / lg n). For 32-bit keys and a million elements, that means four passes of 8 bits each — the standard configuration in real radix sort implementations.

Radix sort is not automatically better than quicksort. It touches every bit of every key, so it is Θ(bn/lg n) where quicksort is Θ(n lg n) in comparisons that often stop after a few bytes. It uses Θ(n + k) extra space and has poor locality. It wins convincingly on large volumes of fixed-width integer keys, and loses on short arrays, variable-length keys, and anything needing a custom comparator.

Bucket sort

Assumption: the input is drawn independently and uniformly from [0, 1).

Divide [0,1) into n equal-sized buckets, drop each element into its bucket, sort each bucket with insertion sort, and concatenate.

BUCKET-SORT(A, n) 1 let B[0:n-1] be a new array of empty lists 2 for i = 1 to n 3 insert A[i] into list B[⌊n·A[i]⌋] 4 for i = 0 to n-1 5 sort list B[i] with insertion sort 6 concatenate lists B[0], B[1], …, B[n-1] in order

Expected running time Θ(n). Lines 1–4 and 6 are Θ(n). For line 5, let nᵢ be the number of elements in bucket i; insertion sort costs O(nᵢ²). Under the uniformity assumption nᵢ is binomial with mean 1, and E[nᵢ²] = 2 - 1/n, so the expected total for line 5 is Θ(n).

Insertion sort is deliberate here rather than lazy: buckets are expected to hold about one element each, and insertion sort has the smallest constant on tiny inputs. Bucket sort is really a demonstration that a good hash of the key space into ordered buckets beats comparison sorting — the same idea behind sample sort and parallel distribution sorts.

Choosing a sort

SortTimeSpaceStableRequires
InsertionΘ(n²), Θ(n) if nearly sortedΘ(1)YesComparisons
MergeΘ(n lg n)Θ(n)YesComparisons
HeapΘ(n lg n)Θ(1)NoComparisons
Quick (rand.)Θ(n lg n) expectedΘ(lg n)NoComparisons
CountingΘ(n + k)Θ(n + k)YesInteger keys in 0..k
RadixΘ(d(n + k))Θ(n + k)YesKeys as d digits
BucketΘ(n) expectedΘ(n)YesUniform over a known range
The three linear sorts do not violate Theorem 8.1 — they sidestep it. Counting sort indexes an array by key value. Radix sort inspects digits. Bucket sort computes a bucket from the value. None of them ever asks “is a less than b?”, so the decision-tree model does not describe them and its bound does not bind them. Extra assumptions about the keys buy you extra speed.

Recap

The seven things to carry forward

Where this goes next

Chapter 9 asks a related question: if you only want the ith smallest element, must you pay for a full sort? No. Randomized selection finds any order statistic in expected Θ(n) time by partitioning like quicksort but recursing on only one side, and the median-of-medians algorithm achieves Θ(n) in the worst case.


Ch 7 — Quicksort Ch 9 — Medians and Order Statistics