Radix Sort
Suppose a warehouse system must sort ten million 64-bit order IDs. Or suppose a network router must sort packets by a 32-bit IP address. Comparison-based sorts like Merge Sort or Quicksort hit a hard wall. No comparison sort can beat Ω(n log n) worst-case time. This is because a decision tree distinguishing n! orderings needs at least log₂(n!) comparisons. Radix sort escapes this wall by refusing to compare keys directly. Instead it exploits the internal structure of a key — its digits. It sorts each digit position using a fast, non-comparative subroutine. This idea matters because so many real keys are structured. Integers, fixed-length strings, IP addresses, dates, and fixed-point currency values are all naturally sequences of digits or characters from a small alphabet. When the number of digits d and the alphabet size k are both small relative to n, radix sort processes n keys in linear time. This is something no comparison sort can promise. It is the workhorse behind large-scale integer and string sorting in database engines, suffix-array construction, and hardware sorting networks. The big picture is a clever reduction. Instead of solving "sort n multi-digit keys" directly, radix sort solves d easier subproblems. Each subproblem is "sort n single-digit keys." The answers are glued together correctly only because each subproblem is solved with a stable sort. Stability is not a nicety here — it is the entire correctness mechanism. Get it wrong and the algorithm silently produces garbage. This article develops the least-significant-digit (LSD) variant precisely. It covers the algorithmic contract with the digit-sorting subroutine, and the induction argument that proves correctness digit by digit. It also covers the Θ(d(n+k)) time bound. Finally, it examines the practical tradeoffs against comparison sorts and against most-significant-digit (MSD) radix variants.
- Radix sort sorts multi-digit keys by repeatedly sorting on one digit at a time, from least significant to most significant.
- It requires the per-digit subsort to be stable; Counting Sort is the standard choice because it is stable and runs in Θ(n+k).
- Correctness follows from an inductive invariant: after processing the low i digits, the array is sorted with respect to those i digits, and stability preserves ordering from prior passes.
- Total running time is Θ(d(n+k)), where d is the number of digits and k is the alphabet size (radix) per digit.
- It beats comparison sorts asymptotically only when d and k are small enough that d(n+k) = o(n log n).
- It is used for sorting fixed-width integers, strings, IP addresses, and as a building block in suffix array and string-processing algorithms.
Sort digit-by-digit from least to most significant using a stable subsort, and stability guarantees the final order is correct.
The idea
A radix is the base of a number system, and radix sort treats each key as a sequence of d digits (or characters) each drawn from an alphabet of size k, for example base-10 digits (k = 10) or bytes (k = 256). The least-significant-digit (LSD) variant processes digit positions in order from the rightmost (least significant) to the leftmost (most significant), performing one full pass over the array per digit position. The subroutine used at each digit position must be a stable sort: a sort that never reorders two elements whose keys (at the digit being examined) are equal. Stability is the load-bearing property of the entire algorithm. Counting Sort is the canonical choice because it is stable by construction and runs in Θ(n + k) time and space, making the whole pipeline linear in each pass. Formally, radix sort is not a new sorting primitive at all — it is a reduction that turns the hard problem of sorting n composite keys into d easy problems of sorting n single-digit keys, glued together by the algebraic guarantee that stability provides. The alternative, most-significant-digit (MSD) radix sort, processes digits from left to right and recursively buckets elements into groups that share a prefix, similar in spirit to quicksort's partitioning. MSD can short-circuit on already-distinguished prefixes and is more natural for variable-length strings, but it needs auxiliary bucketing recursion and careful handling of unequal-length keys. LSD radix sort is simpler, requires only fixed-length (or padded) keys, and is the version whose correctness proof and complexity are cleanest — the focus of this article.
How it works
Fix keys of exactly d digits, digit positions numbered 0 (least significant) through d−1 (most significant), each digit in the range [0, k−1]. LSD radix sort makes d passes over the array; pass i stably sorts the entire array by digit i alone, using the current array order (the result of all previous passes) as the tie-breaker for equal digits.
Step-by-step: 1. Determine d, the maximum number of digits among all keys (pad shorter keys with leading zero-digits conceptually — this does not change their order). 2. For i = 0, 1, 2, …, d−1 (least significant digit first): a. Extract digit i from every key. b. Run a stable counting sort of the whole array using these extracted digits as the sort key. c. Replace the array with the sorted output; proceed to the next digit. 3. After the pass on digit d−1 (the most significant digit), the array is fully sorted. Why does going least-significant-first work, when intuitively you might sort by the "most important" digit first? The trick is that stability lets later (more significant) passes silently preserve the correct relative order established by earlier (less significant) passes whenever the more significant digits tie. Each pass only needs to get its own digit right and trust the sort to leave everything else alone when there's a tie — and that trust is exactly what stability provides. ``` RADIX-SORT(A, d, k) // A[1..n] : array of keys, each with exactly d digits in base k // digit(x, i) returns the i-th digit of x, i = 0 is least significant for i = 0 to d - 1 A = COUNTING-SORT-BY-DIGIT(A, i, k) return A COUNTING-SORT-BY-DIGIT(A, i, k) n = A.length output = new array of size n count = new array of size k, initialized to 0 // 1. Tally how many keys have each digit value for j = 1 to n d_j = digit(A[j], i) count[d_j] = count[d_j] + 1 // 2. Prefix-sum: count[v] becomes the index (one past) of the // last slot reserved for digit value v for v = 1 to k - 1 count[v] = count[v] + count[v - 1] // 3. Place elements from the END of A backwards to preserve // stability: equal-digit keys keep their relative input order for j = n downto 1 d_j = digit(A[j], i) output[count[d_j]] = A[j] count[d_j] = count[d_j] - 1 return output ``` The single most important detail is line 3's backward scan in counting sort: it is what makes the per-digit subsort stable. Scanning forward while placing into the highest available slot for each digit value would still sort correctly by digit value, but would reverse the relative order of ties — breaking the entire induction argument that radix sort depends on.
Complexity & correctness
Time complexity. Each pass calls counting sort once. Counting sort on n keys drawn from an alphabet of size k runs in Θ(n + k): Θ(n) to tally digit occurrences and to redistribute elements, plus Θ(k) to build the prefix-sum array over digit values. With d digit positions, the total cost is ``` Θ(d · (n + k)) ``` If d and k are both O(1) — e.g., sorting 64-bit integers with k = 256-byte digits gives d = 8 — the whole algorithm runs in Θ(n), truly linear time. This is not a contradiction of the Ω(n log n) comparison-sort lower bound, because radix sort never compares two keys against each other; it only inspects digit values, which carries more information per operation than a binary comparison.
Space complexity. Each counting-sort pass needs Θ(n + k) auxiliary space for the output array and the count array; this space is reused across passes, so total auxiliary space is Θ(n + k), not Θ(d(n+k)).
Correctness — proof by induction on digit position. Claim: After the pass processing digit position i (having already processed positions 0 through i−1), the array is sorted with respect to the (i+1)-digit suffix consisting of digit positions 0 through i, i.e., for any two elements x, y, if x's digits 0..i (read as a number) are less than y's digits 0..i, then x precedes y in the array; if they are equal, x precedes y iff x preceded y in the original input array (or, inductively, in the array before this pass, which by the inductive hypothesis reflects original order on ties). Base case (i = 0): after one counting-sort pass on digit 0, the array is sorted by digit 0 alone, and counting sort's stability preserves original relative order among elements with equal digit-0 values. This matches the claim for i = 0. Inductive step: assume the claim holds after processing digit i−1, so the array is sorted on the i-digit suffix (positions 0..i−1), ties broken by original order. Now run counting sort on digit i. Two elements x, y are compared only by their digit-i value:
- If digit_i(x) < digit_i(y), the numeric value of x's (i+1)-digit suffix is smaller regardless of lower digits, so x correctly precedes y. Counting sort places them in this order because it sorts by digit i directly.
- If digit_i(x) = digit_i(y), stability guarantees the relative order from before this pass is preserved. By the inductive hypothesis, that prior order already reflects the correct ordering on the lower i digits, ties broken by original input order. So the array remains correctly ordered on the full (i+1)-digit suffix.
Both cases match the definition of "sorted by the (i+1)-digit suffix, ties broken by original order," completing the induction. Taking i = d−1 (all digit positions processed) shows the final array is sorted by the full d-digit key. ∎ The proof pinpoints exactly why stability is not optional: the "ties preserved from before" step in the inductive argument is a direct citation of the stable-subsort property. Remove stability and the induction collapses immediately at i = 1.
Tradeoffs. Radix sort is not a free lunch:
- It requires digits to be extractable in O(1) time (masking/shifting for integers, indexing for fixed-width strings); it is awkward for keys without a fixed decomposable structure, and for keys compared by an arbitrary user-supplied comparator it does not apply at all.
- Θ(d(n+k)) beats Θ(n log n) only when d(n+k) = o(n log n) — for very large k (e.g., sorting arbitrary 64-bit doubles digit-by-digit with k huge and naive digit choice) it can lose to a good comparison sort or to hybrid approaches.
- It is not in-place: each pass allocates Θ(n + k) auxiliary space, unlike in-place comparison sorts such as Quicksort or Heapsort.
- Choosing the digit size (radix) trades off d against k: fewer, larger digits (bigger k) means fewer passes but a bigger count array and worse cache behavior for large k; more, smaller digits means more passes but tiny k. Practical implementations tune the digit width (commonly 8 or 16 bits) to balance this.
Worked example
Sort the keys 329, 457, 657, 839, 436, 720, 355 (d = 3 decimal digits, k = 10) using LSD radix sort. Below, each row shows the array state and the stable ordering rationale for that pass.
| Stage | Array (in order) | Notes |
|---|---|---|
| Input | 329, 457, 657, 839, 436, 720, 355 | original order, used to break all future ties |
| After digit 0 (ones) | 720, 355, 436, 457, 657, 329, 839 | grouped by ones digit: 0,5,6,7,7,9,9 — ties (457 before 657: both digit-7? check: 457→7, 657→7, tie broken by original order (457 came before 657) |
| After digit 1 (tens) | 720, 329, 436, 839, 355, 457, 657 | grouped by tens digit: 2,2,3,3,5,5,5 — 720 before 329 (tens digit 2, 720 preceded 329 in the previous array); 355,457,657 all tens digit 5, previous relative order 355,457,657 preserved |
| After digit 2 (hundreds) | 329, 355, 436, 457, 657, 720, 839 | grouped by hundreds digit: 3,3,4,4,6,7,8 — fully sorted |
Trace the tricky tie at digit 1 carefully: before this pass the array was 720, 355, 436, 457, 657, 329, 839. Extracting tens digits gives 2, 5, 3, 5, 5, 2, 3. Counting sort tallies: digit 2 appears twice (720, 329), digit 3 appears twice (436, 839), digit 5 appears three times (355, 457, 657). Because the backward scan preserves relative order within each digit bucket, the two elements with tens-digit 5 — 355, 457, 657 — keep exactly the order 355, 457, 657 they had entering this pass, which is precisely why they emerge in that order and, one pass later, fall into their correct final positions (355, 457, 657 already ascending). Had the subsort not been stable, this group could have been scrambled, and no later pass could repair the damage since digit 2 (hundreds) cannot distinguish among numbers that already share the same hundreds digit.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Using an unstable subsort (e.g., naive quicksort) per digit | Breaks the inductive tie-preservation argument | Always use a stable subsort; counting sort is standard |
| Sorting most-significant digit first with a plain per-pass approach | Higher-order passes then get overwritten by later, lower-order passes, destroying earlier correct ordering | Either use LSD order, or use true MSD radix with recursive bucketing that never revisits a distinguished prefix |
| Forgetting to pad keys to equal digit length | Shorter keys silently "misalign" digit positions, corrupting comparisons | Treat missing digits as digit 0, or use a length-aware variant |
| Picking k too large (e.g., digit = whole 32-bit int) | Counting sort's Θ(n+k) term dominates and can exceed Θ(n log n) | Split into fixed-width chunks (bytes or half-words) to keep k small and d bounded |
| Off-by-one in the prefix-sum / placement step of counting sort | Miscounts boundaries, causing overwritten or lost elements | Verify prefix sums exactly partition [1..n]; test on arrays with duplicate keys |
| Assuming radix sort works for arbitrary comparator-based objects | Radix sort needs digit-extractable keys, not a `<` operator | Use radix sort only when keys decompose into fixed-size digits/characters |
Pioneers & history
Radix sort's lineage predates electronic computers entirely: it was mechanized in punched-card tabulating machines used for the U.S. census and, later, for Hollerith-style business data processing, where operators physically sorted decks of cards column by column. The insight that sorting by columns (digits) from least significant to most significant yields a fully sorted deck — using the mechanical "stability" of card sorters that never reordered ties within a pass — is the direct ancestor of the LSD algorithm taught today.
Origins · 1 minHerman Hollerith and how this was discovered
Hollerith designed electromechanical tabulating machines for the 1890 U.S. census, encoding data as holes punched in cards and sorting them column-by-column with card sorters. Sorting a deck by successive columns, low-order to high-order, is mechanically identical to LSD radix sort; his company later became part of IBM, embedding this sorting discipline into decades of data-processing hardware.
Where this matters
Database engines and columnar analytics systems use radix sort (and radix-based hash partitioning) to sort or bucket fixed-width integer and date keys at scale, because Θ(n) behavior on huge tables beats Θ(n log n) comparison sorting when key widths are small and fixed. GPU and parallel sorting libraries favor radix sort because each pass parallelizes cleanly into independent local counting phases followed by a global prefix sum — a pattern well suited to SIMD and multi-core hardware. Suffix array construction algorithms, used in text indexing, bioinformatics genome alignment, and compression, use radix-sort-based ranking of character tuples as an inner loop repeated over increasing prefix lengths. Network routers and switches sort or classify packets by fixed-width fields (IP addresses, port numbers) using radix/trie-based structures descended from the same digit-decomposition idea. Even outside sorting per se, the counting-sort subroutine that powers each radix pass is a building block in bucket-based histogramming, key redistribution in distributed systems, and stable partitioning steps inside more complex algorithms like some variants of Quicksort with three-way partitioning.
Ask the tutor: Can you show me how to adapt LSD radix sort to sort variable-length strings correctly?
Does radix sort violate the Ω(n log n) lower bound for sorting?
No. That lower bound applies specifically to comparison-based sorting algorithms, which must decide the final order using only pairwise `<` comparisons between keys. Radix sort never compares two full keys against each other; instead, it inspects individual digit values and uses counting sort, which reads more structured information (a bounded-range value) per operation than a binary comparison provides. Because the model of computation differs, the information-theoretic argument behind the Ω(n log n) bound (a comparison-based decision tree needs at least log₂(n!) comparisons to distinguish all n! permutations) simply does not constrain radix sort. The apparent "speedup" is really a change of computational model, not a violation of a theorem.
Why is counting sort the standard choice for the per-digit subsort in radix sort?
Counting sort is stable by construction when implemented with the standard backward-scan placement, and it runs in Θ(n + k) time and space for keys drawn from a small range [0, k−1], which is exactly the shape of a single digit. Its stability is not incidental — it is required for the correctness induction that radix sort relies on. Other Θ(n log n) comparison sorts could technically be made stable too, but they would throw away the linear-time advantage that makes radix sort attractive in the first place, since you would then pay Θ(n log n) per digit instead of Θ(n + k).
What happens if the per-digit subsort used is not stable?
The correctness proof breaks at the inductive step: when two keys tie on the current digit, an unstable subsort may reorder them arbitrarily, discarding the correct relative order established by all previous passes. Because later (more significant) passes have no way to "see" or recover the lower digits, this corruption is permanent and typically manifests as isolated, hard-to-spot misorderings involving duplicate values, rather than a total scramble. This is why testing radix sort implementations exclusively on distinct, random keys is dangerous — such tests may never exercise the tie-breaking logic that stability protects.
How do you choose the digit size (radix) in practice?
The choice trades off d (number of passes) against k (per-pass overhead and count-array size). A larger radix (e.g., 16-bit digits, k = 65536) means fewer passes (d shrinks) but a bigger count array per pass, which can hurt cache locality if k is comparable to or larger than n. A smaller radix (e.g., 8-bit digits, k = 256) means more passes but each pass is cheap and cache-friendly. In practice, byte-sized digits (k = 256) are a common default for integer and string keys, since Θ(n + 256) per pass is dominated by the Θ(n) term for any reasonably large n, and the count array fits comfortably in cache.
Can radix sort handle negative numbers or floating-point keys?
Not directly, because its digit-extraction model assumes an unsigned, fixed-width, lexicographically-ordered digit representation. Negative integers are commonly handled by first shifting the whole range to be non-negative (e.g., adding an offset, or flipping the sign bit and treating the value as unsigned) so that ordinary numeric order matches digit-by-digit lexicographic order. IEEE-754 floating-point values can be radix-sorted by a well-known bit trick: flipping the sign bit for positive numbers and inverting all bits for negative numbers converts the bit pattern into an order-preserving unsigned integer, which can then be radix-sorted normally.
How does LSD radix sort compare to MSD radix sort?
LSD processes digits right-to-left over d full passes on the whole array and needs fixed-length keys (or padding), but its correctness proof is a clean digit-by-digit induction using stability. MSD processes digits left-to-right, recursively bucketing elements sharing a prefix into groups and then only continuing to sort within each group's own digit range, which naturally suits variable-length strings and can terminate early once buckets become singletons. MSD is closer in spirit to quicksort's partitioning, is more complex to implement correctly (recursion depth, empty buckets, keys that end mid-recursion), but can be faster on inputs where prefixes discriminate quickly, e.g., sorting strings with distinct first characters.
When should you actually prefer radix sort over a well-tuned comparison sort like quicksort or Timsort?
Prefer radix sort when keys are fixed-width, digit-decomposable, and d(n + k) is meaningfully smaller than n log n — typical for 32/64-bit integers, fixed-length strings, or dates, especially at large n where the constant-factor overhead of extra passes and auxiliary arrays is amortized. Prefer a comparison sort when keys are compared via an arbitrary user-defined comparator, when keys have highly variable structure or length making digit extraction awkward, when memory is tightly constrained (radix sort needs Θ(n + k) extra space per pass), or when n is small enough that constant factors dominate any asymptotic advantage.