Module 20 — Optimum Sorting and Sorting Networks
Source: The Art of Computer Programming, Vol. 3, 2nd ed., §5.3 — §5.3.1 (minimum-comparison sorting) and §5.3.4 (networks for sorting). Lab:
labs/module-20-networks· Grade it:./grade 20This lesson is self-contained: you can complete the module without the book. If you own Vol. 3, read §5.3.1 and §5.3.4 alongside it.
Module 06 taught you how to sort. This module asks the sharper questions: how few comparisons can any sort possibly use, and what if the sequence of comparisons must be fixed in advance, before we see the data? The first question leads to a beautiful lower bound and to Ford and Johnson's merge-insertion algorithm, which is provably optimal for small inputs. The second leads to sorting networks — the data-oblivious sorters that run inside GPU kernels, SIMD sort routines, and hardware switches — and to one of the most elegant lemmas in all of computer science, the zero-one principle.
1. The comparison-tree model and a lower bound
Fix distinct inputs. A comparison sort learns about them only by asking questions of the form "is ?". Every such algorithm, run on all possible inputs, traces out a binary tree: each internal node is one comparison, its two children are the "yes" and "no" continuations, and each leaf is a final answer — a claim about the sorted order, i.e. a permutation of the inputs.
For the algorithm to be correct, every one of the possible input orderings must reach a leaf that reports the right permutation, and two different orderings can never share a leaf (they need different outputs). So the tree has at least leaves.
Theorem (information-theoretic lower bound, §5.3.1). Any comparison sort of elements makes at least comparisons in the worst case.
Proof. A binary tree of height (the length of its longest root-to-leaf path) has at most leaves — level holds at most nodes. The worst-case number of comparisons is exactly the height . Correctness forces , so . Because is an integer, . ∎
This is the first appearance in the course of an adversary / counting argument: we did not analyze any particular algorithm; we bounded all of them at once by counting the outcomes they must distinguish.
Using (Stirling), the bound is : asymptotically , matching mergesort and heapsort — those are asymptotically comparison-optimal.
For small the exact integer values matter:
| n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 3 | 5 | 7 | 10 | 13 | 16 | 19 | 22 | 26 | 29 |
The gap between the bound and the truth
Let be the true minimum — the fewest comparisons that suffice in the worst case, taken over the best possible algorithm. Clearly . Is it equal?
For , yes: . But at the counting bound says 29 while the truth is . There simply is no 29-comparison sort of 12 elements, even though a binary tree of height 29 has room for leaves. The leaves cannot be packed to make every one reachable by a consistent set of comparisons; the geometry of "which comparisons are still informative" forbids it.
is known only for small and remains, in general, an open research problem — Knuth tabulates the frontier and it has barely moved in decades ( also for and ; it differs at – and elsewhere). This is analysis of algorithms as a living science: a bound you can prove in two lines, a truth nobody can compute.
Why it's done this way. The counting bound is cheap and universal, so it is the right first question. When it turns out to be loose (as at ), that looseness is itself the discovery — it tells you the problem has structure the counting argument cannot see, and sends you hunting for a better lower bound or a cleverer algorithm.
Stage 1 has you compute , a counting sort that reports its
comparison count so you can watch a real algorithm sit above the bound, and
is_sorted.
2. Merge insertion (Ford–Johnson), Algorithm 5.3.1M
If is a target, which algorithm hits it for small ? Not straight insertion (quadratic), not binary insertion sort (which wastes comparisons when is not near a power of two). The champion is merge insertion, due to Lester Ford Jr. and Selmer Johnson (1959). Knuth gives it as Algorithm 5.3.1M.
The idea braids merging and binary insertion so that every insertion happens into a range whose size is just under a power of two — so binary search never "wastes" a comparison.
Algorithm M (merge insertion), in Knuth's step style
Given elements to sort:
M1. [Pair up.] Divide the elements into ⌊n/2⌋ pairs (one element is left
over if n is odd). Compare the two members of each pair; call
the larger a_i and the smaller b_i, so b_i < a_i.
M2. [Recurse.] Sort the ⌊n/2⌋ larger elements a_i by merge insertion. Relabel
so a_1 < a_2 < ... , and let b_i be the partner of a_i. The
sequence b_1 a_1 a_2 ... a_s is the "main chain": note
b_1 < a_1, so it goes at the very front for free.
M3. [Insert.] Binary-insert b_2, b_3, ... (and the leftover, if any) into the
main chain — but in a carefully chosen ORDER, not left to right.
Insert them in the groups
(b_3, b_2), (b_5, b_4), (b_11, b_10, ..., b_6), ...
The group boundaries are the Jacobsthal numbers
t_k = (2^(k+1) + (-1)^k)/3 = 1, 3, 5, 11, 21, 43, ...
Within a group, insert from the highest index down.
Why the strange insertion order?
When we insert , we know , so it can only land before in the chain. The number of elements before at that moment is what a binary search must probe — and binary search into a range of size costs comparisons.
Binary search is "efficient" only when is exactly a power of two: inserting into 1 element or 3 elements both cost 2 comparisons, so inserting into 3 is a bargain and inserting into 4 is a rip-off (costs 3). The Jacobsthal grouping picks exactly the indices so that at insertion time the range before has size for the current group : every insertion in group costs exactly comparisons, none wasted. Inserting from the top of each group downward is what keeps the ranges at those magic sizes.
The Jacobsthal numbers (1, 3, 5, 11, 21, 43, …) are precisely the sequence for which "insert group from down to " makes each binary search a perfect fit. (They are also the number of ways to tile a strip with squares and dominoes avoiding two adjacent dominoes — a lovely coincidence Knuth notes.)
Worked trace: n = 5
Start with [3, 1, 5, 2, 4] (using the values as their own ranks).
- M1. Pair. Pairs
(3,1)and(5,2);4is left over. Compare each: , . Two comparisons so far. - M2. Recurse on the larger elements
{3, 5}: one comparison, sorted to . Their partners: (partner of 3), (partner of 5). Main chain: . (Comparisons: 3 total.) - M3. Insert. To insert we have and the leftover
4. Jacobsthal order for the two pending elements (indices 2 and 3, treating the leftover as "b_3"): group boundary , so insert index 3 then index 2:- Insert the leftover
4into the whole chain[1,3,5]by binary search: compare → greater, compare → less →[1,3,4,5]. (2 comparisons; range of size 3, budget .) - Insert , known , into the part before 5, i.e.
[1,3,4]. Binary search: compare → less, compare → greater → position between 1 and 3 →[1,2,3,4,5]. (2 comparisons.)
- Insert the leftover
Total: . Merge insertion is optimal for .
The comparison count
The worst-case number of comparisons satisfies
| n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 3 | 5 | 7 | 10 | 13 | 16 | 19 | 22 | 26 | 30 |
Compare with above: they agree up to , and at merge insertion uses 30 — which is exactly . So for , and . Merge insertion is the comparison-optimal sort for every except a couple of larger cases; Knuth details where it eventually loses (around ) to hand-tuned constructions.
Stage 2 implements Algorithm M as ford_johnson_sort and
ford_johnson_comparisons. The tests check correctness on all sizes 0..=50
and pin the worst case exhaustively for small — you get to watch your code
meet and never exceed it.
3. Sorting networks: comparators, obliviousness, depth
A comparator on wires i and j is a device that reads the values on the
two wires and writes back the smaller on wire i, the larger on wire j. A
sorting network is a fixed sequence of comparators that sorts every input.
The defining feature: the sequence of comparators is oblivious — it does not
depend on the data. There is no "if x < y then compare these next"; the
comparisons are wired in advance. This is what lets a network become silicon, or
a straight-line SIMD routine with no data-dependent branches.
We draw a network with horizontal wires (time flows left to right) and each comparator as a vertical connector:
0 ──●───────●───────────
│ │
1 ──●──●────┼────●───────
│ │ │
2 ─────●────●────┼───●───
│ │ │
3 ──────────●────●───●───
Each ●──● is a comparator; smaller value moves up. Reading the drawing is a
skill: pick any input, walk left to right, and at each connector swap the two
wires if the top value exceeds the bottom.
Batcher's odd–even merge (Algorithm 5.3.4M)
How do we build a correct network? K. E. Batcher's 1968 construction is a network version of mergesort. To sort items:
oddeven_sort(lo, n):
if n > 1:
m = n/2
oddeven_sort(lo, m) # sort the first half
oddeven_sort(lo + m, m) # sort the second half
oddeven_merge(lo, n, 1) # merge the two sorted halves
oddeven_merge(lo, n, r): # merge, r = distance between compared wires
m = 2r
if m < n:
oddeven_merge(lo, n, m) # even-indexed subsequence
oddeven_merge(lo + r, n, m) # odd-indexed subsequence
for i = lo+r, lo+r+m, ... while i + r < lo + n:
compare(i, i + r)
else:
compare(lo, lo + r)
The merge is the clever part: to combine two sorted halves, Batcher recursively merges the even-position elements, merges the odd-position elements, then does a single sweep of "cleanup" comparators between neighbours. That this sorts is not obvious at all — we will prove it in §4 with the zero-one principle rather than by unwinding the recursion.
Comparator count. where the merge uses comparators. Solving: with , i.e. . Concretely:
| n | 2 | 4 | 8 | 16 | 32 |
|---|---|---|---|---|---|
| comparators | 1 | 5 | 19 | 63 | 191 |
Depth. Comparators that touch disjoint wires can fire in the same parallel step. The depth is the fewest such steps — the parallel time. For Batcher's network the depth is exactly
| n | 2 | 4 | 8 | 16 |
|---|---|---|---|---|
| depth | 1 | 3 | 6 | 10 |
To compute the depth of any network, do an as-soon-as-possible schedule:
give each wire a "ready layer" (initially 0); a comparator (i,j) goes to layer
max(ready[i], ready[j]), after which both wires are ready at the next layer.
The largest layer used is the depth. Because we process comparators in a valid
execution order, this ASAP pass computes the critical-path length exactly.
Why networks, when a good comparison sort is only ? Two reasons. Parallel time: a network sorts in depth — with comparators firing at once, wall-clock time is polylogarithmic, ideal for hardware and GPUs. No data-dependent branches: the comparator schedule is fixed, so a network compiles to straight-line code that a CPU pipeline or a SIMD lane never mispredicts. You pay in total work ( vs ) to win latency and predictability. That trade is why small networks ("sorting networks for 4, 8, 16 elements") are hand-inlined into the hottest sort kernels on Earth.
Stage 3 builds odd_even_merge_network, apply_network (the
compare-exchange), and network_depth. The tests pin the counts and depths and
check the network sorts every permutation for .
4. The zero-one principle (Theorem Z)
Checking that a network sorts by trying all permutations is hopeless for (over ). The zero-one principle collapses the work to .
Theorem Z (§5.3.4). A comparator network sorts all inputs (from any ordered set) if and only if it sorts all inputs consisting only of 0s and 1s.
Proof. "Only if" is trivial. For "if": suppose the network sorts every 0-1 input but fails on some input , producing an output that is not nondecreasing. The key lemma is that comparators commute with monotone functions: if is nondecreasing, then running the network on produces where is the output on . This is because a single comparator computes and , and any monotone satisfies and ; apply comparator by comparator.
Now, since is not sorted, there is a position with . Choose the threshold function
is nondecreasing, so the network on outputs . But (since ) and (since ), so has a 1 immediately before a 0 — it is not sorted. Thus is a 0-1 input the network fails to sort, contradicting the assumption. Hence no such exists: the network sorts everything. ∎
This is a reduction by a well-chosen map: turn a hypothetical bad general input into a bad 0-1 input via the threshold trick. It is one of the most reused proof patterns in the theory of parallel and comparison algorithms.
Two payoffs:
- A cheap certificate. To verify a network, enumerate all binary
strings and check each is sorted. For that is tests instead
of . Stage 4's
sorts_all_zero_oneis exactly this certificate. - A design tool. To prove Batcher's construction sorts, argue only about 0-1 sequences — where "sorted" means "all 0s then all 1s" and merging two sorted 0-1 halves is a counting argument about how many 1s land where.
Bitonic sorting
Batcher's other network is the bitonic sorter. A sequence is bitonic if it rises then falls (or is a rotation of such). The bitonic merger takes a bitonic sequence and sorts it; building an ascending run next to a descending run makes their concatenation bitonic, and recursion sorts everything:
bitonic_sort(lo, n, ascending):
if n > 1:
m = n/2
bitonic_sort(lo, m, ascending) # first half ascending...
bitonic_sort(lo + m, m, descending) # ...second half descending
bitonic_merge(lo, n, ascending) # now the whole is bitonic
bitonic_merge(lo, n, dir):
if n > 1:
m = n/2
for i in lo .. lo+m: compare_in_direction(i, i+m, dir)
bitonic_merge(lo, m, dir)
bitonic_merge(lo + m, m, dir)
A "descending" comparator just lists its wires the other way, with , so the same compare-exchange primitive still works. Bitonic sort uses more comparators than odd-even merge (, e.g. 24 for ) but has the same depth and an extremely regular, index-arithmetic structure — which is why GPU implementations overwhelmingly use bitonic sort: every thread computes its partner with a couple of bit operations.
Stage 4 implements sorts_all_zero_one and bitonic_sort_network. You will
certify both Batcher networks via the zero-one principle, then watch a
deliberately broken network (one comparator removed) get caught by a 0-1
counterexample.
5. Stage-by-stage lab guide
Open labs/module-20-networks/src/lab.rs. Run ./grade 20; the grader stops at
the first failing stage.
Stage 1 — min_comparisons_lower_bound, sort_and_count, is_sorted
Compute with a wide-integer factorial and a "smallest with
" loop. sort_and_count can be any correct sort that counts key
comparisons — binary insertion sort is a clean choice. The tests pin the table
of for and dramatize the gap.
Stage 2 — ford_johnson_sort, ford_johnson_comparisons
Implement Algorithm M. The fiddly part is the partner bookkeeping across the recursion and the Jacobsthal insertion order of step M3. A robust design sorts indices by key (so partners survive the recursive sort) and folds the smaller elements in group order. The tests verify correctness for all sizes up to 50 and pin the exhaustive worst case for small .
Stage 3 — odd_even_merge_network, apply_network, network_depth
Transcribe Batcher's recursion into a comparator list; apply_network is a
three-line compare-exchange; network_depth is the ASAP schedule. Counts and
depths are pinned to Batcher's formulas.
Stage 4 — sorts_all_zero_one, bitonic_sort_network
sorts_all_zero_one enumerates binary inputs — the certificate from
Theorem Z. bitonic_sort_network is the bitonic recursion with direction-aware
comparators. The tests certify both networks, cross-check against random
integers, and confirm a broken network fails.
Bench
cargo run --release --example bench -p lab-20-networks --features solutions
times the oblivious network against a data-dependent sort at growing ,
printing n | comps | net | sort | ratio — the constant-factor / branch-free
story made concrete.
6. Check your understanding
- Why must a correct comparison-sort decision tree have at least leaves, and why "at least" rather than "exactly"?
- but . Does this contradict the lower-bound theorem? (No — the theorem gives a lower bound; the truth can be higher.)
- In merge insertion, why is placed at the front of the main chain with no comparison?
- State the zero-one principle. In its proof, what property of a single comparator makes it commute with a monotone function ?
- A network has depth 6 on 8 wires but 19 comparators. How can 19 comparators fit in 6 parallel steps? (Disjoint-wire comparators share a step; up to 4 can fire at once on 8 wires.)
7. Exercises from the text
Ratings are Knuth's (00 immediate · 10 a minute · 20 up to an hour · 30 hours ·
40 term project · 50 open problem). ▶ marks especially instructive ones. Log
attempts in course/module-20-networks/exercises.md.
| Ex. | Rating | Statement (paraphrased) |
|---|---|---|
| 5.3.1-2 | 10 | Show equals corrected — compute both sides for and reconcile. |
| ▶5.3.1-3 | 22 | Prove the worst-case count of merge insertion is . |
| 5.3.1-4 | 20 | Hand-trace merge insertion on ; confirm it uses 26 comparisons. |
| 5.3.1-8 | 24 | For which is ? Tabulate and explain the first failure. |
| ▶5.3.4-1 | 20 | Prove the zero-one principle (Theorem Z) in full. |
| 5.3.4-6 | 22 | Prove Batcher's odd-even merge is correct using the zero-one principle. |
| ▶5.3.4-10 | 28 | Show any sorting network on wires has depth and at least comparators. |
| 5.3.4-38 | 30 | Investigate the minimum-comparator networks for (the "known optimum" table). |
In the real world
- SIMD and vectorized sort. Intel's
x86-simd-sort(the library behind NumPy's and C++std::sort's fast paths) and Google Highway'svqsortsort small blocks with hard-coded sorting networks: for 16 or 32 elements a branch-free network of vectormin/maxinstructions beats any comparison sort because it never mispredicts. The compare-exchange you write inapply_networkis avmin/vmaxpair on real silicon. - GPU sorting. Bitonic sort is the workhorse of GPU sort kernels (CUDA/OpenCL merge-path and bitonic builders): its comparator partners are pure index arithmetic, so every thread knows who to compare with no coordination. The depth is parallel steps — excellent when you have thousands of cores.
- Hardware. Switching fabrics and networking ASICs sort or route with fixed comparator arrays; obliviousness means fixed wiring and constant latency.
- Median filters. Image and signal pipelines find medians of small windows (, ) with tiny optimal networks — a 9-element median network is a classic hand-optimized gadget.
- The AKS network. Ajtai, Komlós, and Szemerédi (1983) built a sorting network of optimal comparators and depth — matching the lower bound asymptotically. But its constant factor is astronomically large (thousands), so it is never used in practice; Batcher's network wins for every realistic . It is the canonical example of an asymptotically optimal algorithm that is practically irrelevant.
- Secure computation. Oblivious sorting is essential in MPC and ORAM, where the access pattern must leak nothing about the data — a data-oblivious network is exactly what you need.
Proof techniques you practiced
- Counting / information-theoretic lower bound — leaves in a binary tree force height , bounding every comparison sort at once (§1).
- Optimality by construction with a matching bound — merge insertion is shown optimal for small by exhibiting an algorithm that meets (§2).
- Amortized "budget" accounting — the Jacobsthal insertion order keeps every binary search inside a range so no comparison is wasted (§2).
- Reduction via a well-chosen map — the zero-one principle turns a bad general input into a bad 0-1 input through a threshold function, using that comparators commute with monotone maps (§4).
- Critical-path (ASAP) analysis — network depth as the longest dependency chain, computed by a one-pass schedule (§3).
8. Where this leads
- Module 21 (Boolean functions) builds straight-line combinational circuits — the comparator networks here are a special case, and the "median/threshold" networks connect directly to symmetric Boolean functions.
- The lower-bound mindset (count the outcomes, bound the tree) recurs for selection, merging, and searching; §5.3.2–5.3.3 in Knuth push it to the median and to merging.
- Data-oblivious algorithms are the entry point to parallel algorithm design and to privacy-preserving computation (ORAM, MPC) — the network is the seed crystal.