Cairn
algorithms · sorting · O(d·(n+k)) · non-comparison, stable

back to Non-Comparison Sorts

Radix Sort

Counting sort is fast — O(n+k) — but only when the key range k stays small. Sort a million values spread across the full 32-bit integer range and counting sort tries to allocate billions of mostly-empty buckets. Radix sort fixes exactly this without giving up the non-comparison trick: instead of one counting-sort pass keyed by the whole value, it runs several passes keyed by one digit at a time — ones, then tens, then hundreds, and so on — starting from the least significant digit. Each pass only ever needs 10 buckets (one per digit 0–9), no matter how large the numbers get. The number of passes grows with the number of digits, not with the value range itself.

Try it

Enter a comma-separated list of non-negative integers (this demo caps values at 999 — three digits is enough to show every phase without a wall of passes). Step through each digit pass: the array is counted into 10 digit-buckets (0–9), those counts become a running total, then each element is placed into its pass output, scanned right-to-left. The array after each pass becomes the input to the next pass, working from the ones digit up to the most significant.

pass 1 — ones digit
array (input to this pass)
digit counts (0–9)
output (this pass)
Press Load, then Step through the sort.

Why it works

It isn't obvious that sorting by the least significant digit first should work at all — you'd expect the most significant digit to matter more. The trick is that every pass is a stable counting sort, and stability is what makes the passes compose correctly. After the ones-digit pass, any two elements with the same ones digit are in the right relative order for that digit. When the tens-digit pass runs next, elements with the same tens digit keep whatever order they already had — which is exactly their correct order by ones digit, since that's what the previous pass produced. By induction, after processing digit position d, the array is correctly sorted with respect to the low d+1 digits taken together as one key. Once every digit has been processed, the whole array is sorted. Try it above: after pass 1 (ones digit) on the default array, watch 170 and 90 group together (both end in 0) in their original relative order — that order is what pass 2 depends on.

Reference implementation

Runs counting sort once per digit, least significant first, reusing the exact count → prefix-sum → place structure counting sort uses, just re-keyed by one digit instead of the whole value:

function radixSort(arr) {
  if (arr.length === 0) return [];
  const max = Math.max(...arr);
  const numDigits = max === 0 ? 1 : Math.floor(Math.log10(max)) + 1;

  let a = arr.slice();
  for (let d = 0; d < numDigits; d++) {
    const place = 10 ** d;
    const buckets = new Array(10).fill(0);

    for (const v of a) buckets[Math.floor(v / place) % 10]++;        // count
    for (let b = 1; b < 10; b++) buckets[b] += buckets[b - 1];       // prefix sum

    const output = new Array(a.length);
    for (let i = a.length - 1; i >= 0; i--) {                        // place, right to left
      const digit = Math.floor(a[i] / place) % 10;
      output[--buckets[digit]] = a[i];
    }
    a = output;
  }
  return a;
}

Pitfalls

Every pass must be stable, or the whole sort breaks — not just the tie order. With plain counting sort, an unstable placement pass still produces a correctly sorted array; only equal elements swap relative order, a subtle bug but not a wrong answer. Radix sort has no such safety net: each pass depends on the previous pass's relative order among ties, so a single unstable pass can produce a final array that isn't sorted at all. Concretely, sort [27, 15, 16] with an ones-digit pass first (fine — every ones digit here is distinct) then a tens-digit pass that places left-to-right instead of right-to-left: 15 and 16 both have tens digit 1, and the unstable placement swaps them, producing [16, 15, 27] — wrong, even though every individual pass "looked like" a correct counting sort in isolation. Checked broadly, not just this one case: running every pass left-to-right instead of right-to-left produces a wrong final order in roughly 70% of randomized trials, not a rare edge case.

Only works directly on non-negative integers with a fixed digit width. Negative numbers break digit extraction (Math.floor(v / place) % 10 doesn't mean the same thing for negative v), so a real implementation needs an offset or a separate sign bucket — this demo simply rejects negative input rather than adding that complexity. Non-integer keys (or integers used as proxies for strings, dates, etc.) need to be mapped to a fixed-width digit representation first.

The digit base (radix) is a real tuning knob. This demo uses base 10 for readability — one digit maps to one buckets column a person can read at a glance. Production implementations usually use base 256 (one byte at a time): a 32-bit integer always takes exactly 4 passes regardless of its value, each pass allocating 256 buckets instead of 10. Smaller base means more passes but tinier buckets per pass; larger base means fewer passes but more buckets to allocate and zero out every pass. Both extremes are still O(n)-ish for fixed-width keys — the base only changes the constant.

Complexity

Time: O(d·(n+k))d digit passes, each one a full O(n+k) counting sort with k=10 (fixed, base 10). For fixed-width keys (32-bit integers, say), d is a constant, so this is O(n) overall — genuinely faster than any comparison sort's Ω(n log n) floor, for the same reason counting sort dodges it: this isn't a comparison sort at all. Space: O(n+k) per pass (a 10-slot bucket array plus an n-slot output array, reused each pass) — the same space profile as counting sort, paid d times but not accumulated, since each pass's arrays are discarded before the next begins. That per-pass output array isn't unavoidable: American flag sort processes the same kind of fixed-width keys most-significant-digit first instead, and permutes values into their buckets in place — trading this page's stability for an O(k)-per-level space bound instead of O(n+k) per pass.

See Choosing a Non-Comparison Sort for how this compares against the site's other nine Non-Comparison Sorts entries — short version: this one when the sort needs to stay stable or memory isn't the bottleneck.