Sorting in Linear Time
The barrier only binds algorithms that compare. By instead using keys as array indices we slip past it: counting sort runs in and is stable, radix sort layers it digit by digit, and bucket sort averages on uniform data.
╌╌╌╌
The previous lesson proved that any sort that learns only by comparing elements needs comparisons. That proof assumes the algorithm extracts information one comparison at a time. If instead we treat keys as data we can read, using a key directly as an array index or splitting it into digits, the decision-tree argument no longer applies, and we can sort in linear time.1 The tradeoff is generality: these algorithms need keys drawn from a small or structured universe, not arbitrary comparables.
How the lower bound is escaped
The bound counts the branchings of a decision tree whose only moves are comparisons: with possible orderings and two outcomes per comparison, at least comparisons are needed to distinguish them. A key used as an index makes a move the tree cannot: it routes an element to one of slots in a single operation, a -way branch that no binary comparison tree models. The moment an algorithm reads a key's value directly — rather than only its order relative to another key — the decision-tree argument stops applying, and the floor it imposes is no longer binding.
This works only when the key universe is small or structured enough to index into: integers in a bounded range (counting sort), integers split into a bounded number of digits (radix sort), or reals whose distribution is known (bucket sort). On arbitrary comparable objects there is nothing to index on; the only remaining move is to compare, and the bound applies again.
Counting sort
Suppose every key is an integer in the range . Counting sort never compares two elements. Instead it counts, for each value , how many keys are ; that count gives the final position of the last key equal to . Reading the input back-to-front and decrementing as we place, we drop each element straight into its sorted slot.
- 1let be a new array
- 2for to do
- 3
- 4for to do
- 5C[v] = count of keys = v
- 6for to do
- 7C[v] = count of keys
- 8for downto do
- 9
- 10next equal key goes before it
The first count loop tallies occurrences; the prefix-sum loop turns counts into ranks (how many keys land at or before each value); the final loop scatters each element into its slot in the output array . Walking the input from down to is what makes the sort stable. Equal keys are emitted in their original relative order, because the last such key claims the highest of the slots reserved for that value, and earlier ones fill in below it.
Trace it on . The count loop tallies each
value: two s, no s, two s, three s, no s, one , giving
. The prefix-sum loop replaces each entry with the
running total, . Read this as ranks:
says seven keys are
, so the last belongs in output slot
. Each value's rank now gives the exact output index of its largest copy, computed
without a single comparison between two keys.
One step of the scatter loop shows both the mechanism and the stability. Reading from the back, the last key looks up its rank and drops straight into ; we then decrement to , so the next we meet (an earlier one in ) lands in , just before it, preserving input order.
Analysis. The loops run , , , and times, so counting sort is in both time and space. As long as this is , genuinely linear, beating the comparison bound because no comparisons happen.2 The limitation is the space and time in . If the keys range over, say, -bit integers, then dwarfs any realistic , the count array is enormous, and the method is impractical. Counting sort works best when the key universe is small.
Radix sort
What if the keys are larger, say -digit numbers, so that a single counting pass is infeasible? Radix sort decomposes each key into digits and sorts one digit at a time. The counterintuitive rule, known since the days of punched-card machines, is to sort by the least significant digit first (LSD), working up to the most significant.
- 1for to do
- 2use a stable sort to sort on digitdigit 1 = least sig.
The correctness rests entirely on stability.
Using an unstable per-digit sort would destroy the work of every earlier pass. This is why the inner sort must be stable, and counting sort is the natural choice.
A single pass shows why stability is required. Suppose the array is already ordered on the low digit, and we now sort on the next one. Keys that tie on the new digit must keep their incoming order, since that order already encodes the lower digit; only keys that differ on the new digit may be reordered.
Analysis. With counting sort on each of digits, each drawn from a range of size , every pass costs , for a total of
When is a constant and , for example fixed-width integers split into a constant number of digits in a base of size , radix sort runs in . Choosing the digit size is an engineering tradeoff: larger digits mean fewer passes ( shrinks) but a larger per pass. For -bit keys, the best choice is typically digits of about bits, so and .
Consider -bit keys with elements. Splitting into -bit digits gives passes over a count array of size ; each pass is , for total. Splitting into -bit digits gives passes but a count array of size , comparable to itself; the total is again, but the larger strains the cache. Halving the digit size the other way — -bit digits — doubles to passes with a tiny . The product is what to minimize, and the sweet spot keeps near .
Bucket sort
Counting and radix sort exploit integer keys. Bucket sort instead exploits a distributional assumption: that the keys are drawn (roughly) uniformly at random from an interval, say . It scatters the keys into equal sub-intervals, the buckets, sorts each bucket with a simple sort like insertion sort, then concatenates the buckets in order.
- 1
- 2let be an array of empty lists
- 3for to do
- 4insert into listbucket by value
- 5for to do
- 6sort list with insertion sort
- 7concatenate in order
Scattering is , and concatenation is . The only variable cost is sorting the buckets. If the input is spread uniformly, each bucket holds about one element on average, so the insertion sorts cost each in expectation.4
Analysis. Let . Insertion sort on bucket costs , so the expected total bucket-sorting cost is . Each key lands in bucket independently with probability , so is Binomial, which has
Summing over the buckets gives , so the total expected running time is
This is an average-case result: it assumes the inputs are uniformly distributed. Adversarial input, with every key landing in the same bucket, degrades bucket sort to the of a single insertion sort. Bucket sort is the right tool when you know your data is spread evenly (or can cheaply map it so), as with fractional parts of well-mixed values.
A worked bucket sort
Take the keys , uniform-looking values in . Each key lands in bucket , so , , , and so on. Scattering costs one pass:
| bucket | keys placed (in arrival order) |
|---|---|
| — | |
Buckets , , , and stay empty. Insertion sort now orders each bucket's short list — bucket becomes , bucket becomes , bucket becomes — and reading the buckets left to right concatenates them into the sorted output. No bucket held more than three keys, so every insertion sort was work, and the whole sort touched each key a constant number of times.
Choosing among them
None of these linear-time sorts is a drop-in replacement for a comparison sort like mergesort or heapsort. Each rests on a structural assumption about the keys, so the choice comes down to matching the algorithm to what you know about your data.5
| Algorithm | Assumption on keys | Time | Stable? | Extra space |
|---|---|---|---|---|
| Counting sort | integers in a small range | yes | ||
| Radix sort | digits, each in a small range | yes | ||
| Bucket sort | reals spread uniformly over an interval | expected | yes |
Practical guidance:
- Use counting sort when keys are integers over a range comparable to (grades, small ages, byte values). It is also the standard stable subsort inside radix sort.
- Use radix sort for fixed-width keys with a larger range, such as - or -bit integers or fixed-length strings, where a single counting pass would need an impossibly large count array.
- Use bucket sort when keys are real numbers believed to be uniformly (or near-uniformly) distributed, and linear expected time suffices.
These methods beat precisely because they are not comparison sorts: they compute with the keys rather than comparing them. On arbitrary comparable objects with no exploitable integer or distributional structure, the linear-time guarantee is gone, and a comparison sort with its bound is the only option.
Radix sort in practice
The textbook radix sort scatters into separate output lists per pass, paying auxiliary space. In production that copying and the poor cache behavior of scattered writes are the bottleneck, and two refinements address them.
MSD radix, in place: American flag sort. Sorting most-significant digit first lets a radix sort partition the array in place, the way quicksort does, rather than into external buckets. American flag sort (McIlroy, Bostic, and McIlroy, 1993) makes two passes over the array per digit: the first counts how many keys fall in each of the digit values, turning the counts into bucket boundaries; the second permutes elements into place by following a cycle of swaps, so each key is moved directly to its bucket with no auxiliary array. It then recurses on each bucket for the next digit. The in-place permutation trades counting sort's scratch space for a swap-heavy inner loop, and because it is MSD it can stop early on distinguishing prefixes — the standard choice for sorting large string sets where keys share long common prefixes.
Adaptive bucketing: spreadsort. Bucket sort's fragility is its fixed uniform partition; real data is rarely uniform. Spreadsort (Ross, 2002; shipped in the Boost C++ libraries) is a hybrid that inspects the actual range of the keys, sizes its buckets to that range rather than assuming , and recursively spreads or falls back to a comparison sort when a bucket is small enough that partitioning no longer pays. It interpolates between radix sort's digit-splitting and quicksort's divide-and-conquer, achieving close to linear time on real numeric data without bucket sort's uniform-distribution assumption or radix sort's fixed digit width.
Where linear sorts actually run. Radix sort is the standard high-throughput sort on GPUs: a GPU has thousands of lanes but suffers from the branch divergence of a comparison sort's data-dependent control flow, whereas a radix pass is a fixed sequence of counts and scatters that maps cleanly onto parallel prefix-sums (Merrill and Grimshaw, 2011). Column-store databases likewise radix-sort fixed-width integer and date columns, and MapReduce-style systems partition keys by a radix-like hash to route them to reducers. The common pattern: when the keys have exploitable structure, computing with them beats comparing them, and the advantage is largest on wide parallel hardware and data too large to shuffle randomly.5
Takeaways
- The bound binds only comparison sorts; using keys as array indices or digit sequences sidesteps it entirely.
- Counting sort ranks keys by prefix-summing their counts: , stable, linear when but impractical when is large.
- Radix sort stably sorts digit by digit, least significant first; stability is what preserves earlier passes, giving .
- Bucket sort scatters uniform keys into buckets and sorts each; expected , but if the distribution is adversarial.
- Each linear sort trades generality for a structural assumption on the keys, so choose by what you actually know about your data.
Footnotes
- Erickson, Algorithms, Ch. — Sorting Beyond Comparisons — treating keys as readable data sidesteps the decision-tree argument and permits linear-time sorting. ↩
- CLRS, §8.2 — Counting Sort — counting sort runs in , is stable, and is linear when . ↩
- CLRS, §8.3 — Radix Sort — sorting least-significant digit first with a stable subsort yields a correct sort in . ↩
- CLRS, §8.4 — Bucket Sort — scattering uniformly distributed keys into buckets gives expected running time. ↩
- Skiena, The Algorithm Design Manual, §4 — Sorting and Searching — choosing the right sort by matching the algorithm to the structure of the keys. ↩ ↩2
╌╌ END ╌╌