Algorithms
Radix Sort
Sort by digits — O(d·n) for d-digit keys, beats comparison sorts for fixed-width data
Radix sort orders elements by processing digits one at a time, from least significant to most significant. Each pass is a stable counting sort on one digit. Total cost — O(d × (n + b)) where d is the number of digits and b is the radix (digit alphabet size). For 32-bit integers with byte-by-byte radix sort, that's 4 passes over n elements — linear in n, regardless of value range.
- TimeO(d × (n + b))
- SpaceO(n + b) — output array + count buckets
- StableYes (required for correctness)
- Beats Ω(n log n)Yes — non-comparison sort
- Best forFixed-width integer or string keys
- VariantsLSD (least-sig digit first), MSD (most-sig first), three-way radix quicksort
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
How LSD radix sort works
Sort the array by the least-significant digit, then by the next, and so on up to the most-significant. Each pass uses a stable counting sort. After processing all digits, the array is fully sorted.
Concrete walkthrough. Sort [170, 45, 75, 90, 802, 24, 2, 66] by base 10 digits:
Initial: [170, 45, 75, 90, 802, 24, 2, 66]
By units (1s): [170, 90, 802, 2, 24, 45, 75, 66] (last digits: 0, 0, 2, 2, 4, 5, 5, 6)
By tens (10s): [802, 2, 24, 45, 66, 170, 75, 90] (sort stable by tens: 0, 0, 2, 4, 6, 7, 7, 9)
By 100s: [2, 24, 45, 66, 75, 90, 170, 802] (sort stable by hundreds: 0, 0, 0, 0, 0, 0, 1, 8)
Three passes, three counting sorts. Each pass costs O(n + 10) for base 10 (n elements + 10 buckets). Three passes total: O(3 × n) = O(n). For 1M elements, ~3M operations vs ~20M for O(n log n) comparison sorts.
Byte-by-byte radix sort for 32-bit integers
For practical use on integers, base 256 (one byte per pass) is the sweet spot:
function radixSort32(arr) {
const n = arr.length;
let buf = new Uint32Array(n);
let src = Uint32Array.from(arr);
const count = new Uint32Array(256);
for (let shift = 0; shift < 32; shift += 8) {
count.fill(0);
// Phase 1: count occurrences of each byte at this position
for (let i = 0; i < n; i++) {
count[(src[i] >>> shift) & 0xFF]++;
}
// Phase 2: cumulative sum
for (let i = 1; i < 256; i++) count[i] += count[i - 1];
// Phase 3: write to buffer in stable order (right-to-left)
for (let i = n - 1; i >= 0; i--) {
const byte = (src[i] >>> shift) & 0xFF;
buf[--count[byte]] = src[i];
}
// Swap src and buf for next pass
[src, buf] = [buf, src];
}
return Array.from(src);
}
Four passes, each O(n). For 10M random 32-bit integers, this radix sort is typically 2-5× faster than the JavaScript built-in sort() (which uses Timsort on most engines). The advantage grows for larger arrays.
Radix sort vs counting sort
| Counting sort | Radix sort | |
|---|---|---|
| Time | O(n + k) where k = value range | O(d × (n + b)) where d = digits, b = radix |
| Space | O(n + k) | O(n + b) |
| Best for | Tiny value range (k < n) | Large value range, fixed digits |
| k = 1M, n = 1K | O(1M) — wasteful | O(d × 1K) — efficient |
| k = 10, n = 1M | O(1M) — fastest | O(d × 1M) — slower |
| 32-bit integers | O(n + 4 × 10⁹) — impossible | O(4 × n + 4 × 256) — perfect |
The core difference — counting sort is one pass over a (potentially huge) value range; radix sort is many passes over a (small) digit range. When the value range fits in memory comfortably, counting sort wins. When it doesn't, radix sort handles it.
When to use radix sort
- Sorting fixed-width integer keys. 32-bit, 64-bit, IPv4 addresses, fixed-length numeric IDs. Byte-by-byte radix sort is faster than any comparison sort.
- Sorting fixed-length strings. ISBN codes, telephone numbers, fixed-format identifiers. MSD radix sort with one digit per pass.
- Database query engines. Many embedded sorts in modern OLAP databases (DuckDB, ClickHouse) use radix sort variants when keys are integer.
- External sorting (sort-merge of huge data). Pre-radix-sort each input chunk in memory, then merge. The memory-resident phase exploits radix sort's cache friendliness.
- Suffix array construction. Specialized variants of radix sort (DC3, SAIS) compute suffix arrays in linear time, supporting full-text search and bioinformatics.
MSD radix sort and three-way radix quicksort
For variable-length string keys, LSD radix sort is wasteful — short strings get padded with implicit zero digits and processed redundantly. MSD radix sort (most-significant digit first) processes from the left, recursing into each bucket. Once a bucket has a single element (or all elements in the bucket are equal to that point), recursion stops.
Three-way radix quicksort (Bentley + Sedgewick) combines this with quicksort's partitioning. Three partitions per recursion: less-than-pivot-character, equal-to-pivot-character, greater-than. Recurse on all three; the equal-to recursion advances to the next character. Best in class for sorting general string arrays — beats both pure quicksort (no character-level optimization) and pure MSD radix (overhead per character).
Python implementation
def radix_sort_lsd(arr, base=10):
if not arr: return arr
max_val = max(arr)
exp = 1
out = list(arr)
while max_val // exp > 0:
out = counting_sort_by_digit(out, exp, base)
exp *= base
return out
def counting_sort_by_digit(arr, exp, base):
n = len(arr)
output = [0] * n
count = [0] * base
# Phase 1: count
for x in arr:
count[(x // exp) % base] += 1
# Phase 2: cumulative
for i in range(1, base):
count[i] += count[i - 1]
# Phase 3: stable writeback (right-to-left)
for i in range(n - 1, -1, -1):
digit = (arr[i] // exp) % base
count[digit] -= 1
output[count[digit]] = arr[i]
return output
Common radix sort bugs
- Using an unstable sort per pass. Stability isn't optional — non-stable per-digit sorting destroys the ordering from previous passes. Always use counting sort with right-to-left writeback (or any other stable sort) per pass.
- Forgetting to handle negative numbers. Standard radix sort treats numbers as unsigned. For signed integers, either bit-cast and flip the sign bit (so positives sort above negatives), or split into negative and positive subarrays, sort each, concatenate.
- Wrong digit extraction. For LSD, the k-th pass extracts digit k from each number. Off-by-one in the shift / divisor produces a sort that "looks right" but is actually corrupted. Test with adversarial inputs (sorted, reversed, all-equal, all-different).
- Using radix sort for tiny n. For n < 100, the overhead (allocate buckets, multiple passes) makes radix sort slower than insertion sort. Hybrid algorithms switch to a comparison sort for small subarrays.
- Allocating new buckets per pass. Reuse the count array (zero-fill it) instead of allocating fresh — typical 2-3× perf improvement on hot loops.
Frequently asked questions
How does radix sort beat the Ω(n log n) lower bound?
That bound applies only to comparison sorts. Radix sort never compares two elements directly — it bucketizes by digit value. Like counting sort, it exploits structure in the keys (specifically, that they have a bounded number of digits with bounded alphabet). The bound doesn't apply because the algorithm doesn't compare.
What's the difference between LSD and MSD radix sort?
LSD (Least Significant Digit) processes from rightmost digit to leftmost. Each pass is a stable counting sort. After d passes, the array is fully sorted. Simple, iterative. MSD (Most Significant Digit) processes from leftmost first, recursing into each bucket. More complex but allows early termination — variable-length keys can stop processing once the bucket is unique. LSD is the default; MSD is preferred for variable-length string keys.
Why must each pass be stable?
After sorting on digit k, the array is sorted by digits 0 to k. Sorting on digit k+1 must preserve the existing order for elements with the same digit-(k+1) value — otherwise, we lose the work from previous passes. Counting sort with right-to-left writeback is stable; using an unstable sort per pass produces incorrect output.
When is radix sort faster than quicksort?
For fixed-width integer or string keys, radix sort's O(d·n) beats quicksort's O(n log n) when d < log₂(n). For 32-bit integers (d=32 or 4 bytes) on 1B elements (log₂ n ≈ 30), they're competitive — and radix sort wins on cache locality + no comparisons. For variable-length string keys with rare prefixes, quicksort or three-way radix quicksort wins.
What radix should I use?
Bigger radix = fewer passes but more buckets. Byte-sized radix (b=256) is the sweet spot for most modern hardware — 4 passes for 32-bit integers, fits in L1 cache, plays well with SIMD. b=10 (decimal digits) is purely educational. b=2 (binary radix sort) gives minimum buckets but most passes — a tradeoff that matters only on memory-constrained systems.
Can radix sort handle floating-point numbers?
Yes, with bit-tricks. Reinterpret a float as an integer (bit-cast). Flip the sign bit for positives, flip all bits for negatives — this transforms IEEE 754 floats into integers whose unsigned ordering matches the float ordering. Radix sort, then reverse the transformation. Used by some database engines.
How is radix sort related to bucket sort?
Bucket sort distributes elements into buckets by value range, then sorts each bucket independently. Radix sort distributes into buckets by digit value, then re-distributes by the next digit. They're cousins — both bypass comparison sort by partitioning on key structure. Radix sort works deterministically across passes; bucket sort relies on the value distribution being uniform.