Part III · Classical Algorithms Reconstructed
Chapter 9
Searching and Sorting Reconstructed
mixed Classical algorithms with standard proofs; each is mapped to its grammar, including where the grammar adds nothing.
- State, implement and prove correct linear search, binary search, insertion sort, merge sort, heapsort, quicksort and counting sort.
- Predict their comparison counts exactly or within stated bounds, and check the predictions with counted costs.
- Prove the lower bound for comparison sorting, and explain it as a statement about the grammar of comparisons.
- Explain why counting sort escapes the bound — arithmetic addressing — and exactly when it stops helping.
Each algorithm in Part III is presented as an algorithm card: the problem, the conventional algorithm with pseudocode and a correctness argument, its costs, a pointer to its SDK implementation, its CGT reading (which grammar it operates on, and how grammar operations map to implementation steps), and — always — where the grammatical view adds nothing. This chapter’s verdict is mostly equivalence: sorting and searching are the home ground of classical algorithm design, and the grammatical view re-describes them without making them faster. That verdict is itself a result.
9.1 Searching
Problem. Given a sequence and a value , find an index with , or report that none exists.
lo ← 0; hi ← n invariant: a[lo-1] < x ≤ a[hi] (sentinels a[-1] = −∞, a[n] = +∞)
while lo < hi:
mid ← ⌊(lo + hi)/2⌋
if a[mid] < x: lo ← mid + 1 else: hi ← mid
return lo if lo < n and a[lo] = x else −1
On a sorted sequence, binary search returns the leftmost index of or , using at most loop comparisons plus one final equality test. Linear search uses at most comparisons and needs no order.
The invariant holds initially and is preserved by both branches because the sequence is sorted; the interval strictly shrinks and at least halves, so the loop runs at most times. At exit , and the invariant says every element before is and ; hence occurs iff , and then is its leftmost index.
∎SDK: cgtsdk.algorithms.binary_search, linear_search. Grammar: binary search is navigation in the implicit balanced search tree whose root is the middle element: each comparison is a move or , and the comparison outcomes spell the address of the answer. The sorted array is a perfectly balanced BST in disguise, with addresses computed by midpoint arithmetic — arithmetic addressing again (M-ARITH). Where grammar adds nothing: the algorithm is optimal among comparison-based searches (an adversary argument gives ), and no re-description changes that.
9.2 Four comparison sorts
Problem. Rearrange a sequence into non-decreasing order.
The four classical comparison sorts, in their SDK form:
| Algorithm | Comparisons (worst) | Comparisons (best) | Extra space | Stable | Reference |
|---|---|---|---|---|---|
| insertion sort | (sorted input) | yes | folklore | ||
| merge sort | about | yes | [Cormen et al. 2022] | ||
| heapsort | no | [Williams 1964] | |||
| quicksort (random pivot) | expected | no | [Hoare 1962] |
for i ← 1 to n−1: invariant: a[0..i) is sorted
x ← a[i]; j ← i − 1
while j ≥ 0 and a[j] > x: a[j+1] ← a[j]; j ← j − 1
a[j+1] ← x
The invariant gives correctness at once. The comparison count is plus the number of inversions (pairs with ), capped per element by its position — exactly on sorted input and on reversed input. The SDK counts confirm both exactly:
import random
from cgtsdk import Cost
from cgtsdk.algorithms import heapsort, insertion_sort, merge_sort, quicksort
n = 512
inputs = {"sorted": list(range(n)), "reversed": list(range(n, 0, -1)),
"random": random.Random(1).sample(range(10**6), n)}
print(f"{'input':9s} {'insertion':>10s} {'merge':>7s} {'heap':>7s} {'quick':>7s} n(n-1)/2={n*(n-1)//2} n*log2(n)={n*9}")
for name, a in inputs.items():
counts = []
for f in (insertion_sort, merge_sort, heapsort, quicksort):
c = Cost(); assert f(a, c) == sorted(a); counts.append(c.counts.get("compare", 0))
print(f"{name:9s} {counts[0]:10d} {counts[1]:7d} {counts[2]:7d} {counts[3]:7d}")
input insertion merge heap quick n(n-1)/2=130816 n*log2(n)=4608
sorted 511 2304 7958 8642
reversed 130816 2304 7203 8217
random 69565 3970 7599 7796
(Quicksort here uses a three-way partition that charges up to two comparisons per element per partitioning step, which is why its counts sit above merge sort’s.) Insertion sort is the best of the four on sorted input and by far the worst on reversed input; merge sort makes exactly comparisons when one half always wins, and fewer than in every case.
Text alternative: choose , an input order and a seed; the widget sorts the same input with insertion sort, merge sort and heapsort, counting comparisons exactly as the SDK does, and compares them with and .
9.3 The comparison lower bound as a statement about grammar
A comparison sort can learn about its input only by comparing elements. Its execution on all inputs of size is therefore described by a decision tree: a binary tree whose internal nodes are comparisons “?” and whose leaves are output permutations. Each run follows one root-to-leaf path; the path’s address — the word of comparison outcomes — determines the output.
Every comparison-based sorting algorithm makes at least comparisons on some input of size .
Distinct inputs that are permutations of require distinct outputs, so the decision tree has at least leaves. A binary tree of height has at most leaves, so , and by Stirling’s formula.
∎In the language of this book: the grammar available to a comparison sort is the binary alphabet of comparison outcomes, and the information it must produce is one of answers. No grammar over a two-letter alphabet can address answers with words shorter than . This is the counting lemma that reappears in Chapter 12 as an information lower bound on any precomputed grammar (CGT-THM-002). Re-describing sorting grammatically cannot beat it — and claims to “sort in constant time with a grammar” should be read against it.
9.4 Counting sort: escaping the bound by arithmetic addressing
The lower bound assumes that comparisons are the only way to learn about the input. If the keys are integers in a small range , a key can be used as an address:
count[0..k] ← 0
for x in a: count[x+1] ← count[x+1] + 1 histogram, using x as an address
for i ← 0 to k−1: count[i+1] ← count[i+1] + count[i] prefix sums: first slot of each key
for x in a (left to right): out[count[x]] ← x; count[x] ← count[x] + 1 stable placement
Counting sort sorts integers in stably with no comparisons, in time and space.
This is mechanism M-ARITH in its purest form: the key’s value is the location, exactly like an array index (Chapter 2). And it has exactly the array’s limitation: the density must not be small. With it is linear; with 64-bit keys () it is absurd. Radix sort refines the idea by addressing one digit at a time.
import random
from cgtsdk import Cost
from cgtsdk.algorithms import counting_sort, merge_sort
rng = random.Random(2)
for n, k in ((10_000, 100), (10_000, 10_000), (10_000, 1_000_000)):
a = [rng.randrange(k) for _ in range(n)]
c1, c2 = Cost(), Cost()
assert counting_sort(a, k, c1) == merge_sort(a, c2) == sorted(a)
print(f"n={n} k={k:9d} counting sort: {c1.total:9d} steps, 0 comparisons merge sort: {c2.total:7d} steps")
n=10000 k= 100 counting sort: 50301 steps, 0 comparisons merge sort: 253841 steps
n=10000 k= 10000 counting sort: 80001 steps, 0 comparisons merge sort: 254082 steps
n=10000 k= 1000000 counting sort: 3050001 steps, 0 comparisons merge sort: 254093 steps
At density keys per slot, counting sort is five times cheaper than merge sort in counted steps; at density it is twelve times more expensive. The same algorithm wins or loses according to a single structural parameter — the density of the address space.
9.5 Verdict
For searching and sorting, the grammatical view gives re-descriptions: binary search as navigation in an implicit tree, a sorting algorithm as a grammar of comparison outcomes, counting sort as arithmetic addressing. It gives no speedup over the classical algorithms, and the comparison lower bound explains why none is possible within the comparison model. What it does give is a uniform vocabulary in which the one known way around the bound — using keys as addresses — is the same mechanism as array indexing and heap navigation, with the same failure condition.
Prove that insertion sort makes exactly comparisons, where is the number of inversions and is the number of elements that are smaller than every element before them (those for which the inner loop runs off the left end). Check the formula against the SDK on random inputs.
Give an input of size on which the SDK’s quicksort with seed makes more comparisons than heapsort, or prove that none exists. (Use the SDK to search.)
Show that comparisons suffice to sort elements (seven comparisons), or look up and verify a schedule that does.
Counting sort uses memory even when only a few distinct keys occur. Combine it with hashing (Chapter 4) to sort keys from a huge universe with distinct values in time. Which mechanisms does your algorithm use?
Solution to Exercise 9.4
Count occurrences in a hash table keyed by value ( expected, M-ARITH with collisions), sort the distinct keys by a comparison sort (), then emit each key its counted number of times (). The bound is expected, under the hashing assumptions of Chapter 4.
- Binary search is navigation in an implicit balanced tree addressed by midpoint arithmetic; it is optimal among comparison searches.
- Insertion, merge, heap and quick sort are implemented with counted comparisons that match their analyses exactly where the analysis is exact.
- The lower bound is a counting statement: a binary grammar of comparison outcomes cannot address answers with shorter words.
- Counting sort escapes it by using keys as addresses — arithmetic addressing — and is only as good as the density of the key range.
- For searching and sorting the grammatical view yields equivalence, not improvement; that is a finding, not a failure.
References cited in this chapter
- Cormen et al. 2022 T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein (2022). Introduction to Algorithms, 4th edition. MIT Press (ISBN 978-0-262-04630-5).
- Hoare 1962 C. A. R. Hoare (1962). Quicksort. The Computer Journal 5(1):10–16. doi:10.1093/comjnl/5.1.10.
- Williams 1964 J. W. J. Williams (1964). Algorithm 232: Heapsort. Communications of the ACM 7(6):347–348.