I want to start with a simple question I asked myself when I first learned sorting algorithms: is it actually possible to sort a list faster than $O(n \log n)$? Every comparison-based algorithm I had studied up to that point — QuickSort, Merge Sort, Heap Sort — seemed to hit that same wall. Then I came across Radix Sort, and it changed how I thought about sorting entirely.
Radix Sort doesn’t compare elements against each other at all. Instead, it sorts numbers digit by digit, using a stable sorting subroutine (usually Counting Sort) as its engine. Because it sidesteps comparisons, it can sort in linear time under the right conditions. I find this a genuinely elegant idea, and in this article I want to walk through it the way I understood it myself — from history to implementation.
History and Background
The roots of Radix Sort go back further than computer science itself. I learned that the core idea traces back to punched card sorting machines used in the early 20th century, particularly Herman Hollerith’s tabulating machines from the 1890s, which processed census data using mechanical card sorters that worked digit by digit.
The algorithmic formalization I use today came later, with Harold H. Seward credited with describing the LSD (Least Significant Digit) Radix Sort in 1954 at MIT. Donald Knuth later documented and analyzed the algorithm thoroughly in The Art of Computer Programming, Volume 3: Sorting and Searching, which is where most modern treatments (including mine) draw their theoretical grounding.
What strikes me about this history is that Radix Sort predates electronic computers. The mechanical sorting machines that inspired it were literally physically sorting cards by punching patterns representing digits — a beautifully tactile origin for an algorithm I now think of as purely abstract.
Problem Statement
The problem I’m solving here is the same one every sorting algorithm addresses: given a list of $n$ elements, arrange them in non-decreasing (or non-increasing) order.
But Radix Sort is designed for a specific flavor of this problem: sorting integers (or fixed-length strings) where each element can be decomposed into a sequence of digits or characters. The comparison-based sorting lower bound tells me that any algorithm relying purely on comparisons cannot beat $O(n \log n)$ in the worst case. Radix Sort’s whole purpose is to escape that boundary by exploiting the structure of the keys themselves rather than comparing them pairwise.
Core Concepts
Before I get into the mechanics, I want to define the vocabulary I’ll be using:
- Digit: A single symbol position within a number, when the number is expressed in some base (radix) $b$. In base 10, the number 802 has digits 8, 0, and 2.
- Radix (base): The number of unique digit values, such as 10 for decimal or 2 for binary.
- LSD (Least Significant Digit) Radix Sort: Processes digits starting from the rightmost (units place) and moves toward the leftmost (most significant digit).
- MSD (Most Significant Digit) Radix Sort: Processes digits starting from the leftmost, which is more natural for sorting strings of varying length but is more complex to implement correctly.
- Stable sort: A sort that preserves the relative order of elements with equal keys. This property is essential for Radix Sort to work — I’ll explain why shortly.
- Bucket: A temporary grouping used during each digit pass, typically indexed 0 through $b-1$.
How It Works
I use LSD Radix Sort in this article since it’s the most common variant and it’s what I implement below. Here is the step-by-step process I follow:
- Find the maximum number in the array to determine the number of digits I need to process.
- Starting with the least significant digit (units place), sort the entire array based on that digit using a stable sorting algorithm (I use Counting Sort).
- Move to the next digit (tens place) and sort the array again, but this time based on that digit — critically, using the already partially sorted array from the previous pass.
- Repeat this process for every digit position, up to the digit count of the largest number.
- Once I’ve processed the most significant digit, the array is fully sorted.
Working Principle
The internal logic here relies entirely on stability. Every single pass I make (one per digit) must preserve the relative order of elements that share the same digit value in the current position. If I use an unstable sort within each pass, previous work gets destroyed and the final result is wrong.
Here’s the intuition I hold onto: after I finish the pass on the units digit, the array is correctly sorted with respect to that digit — meaning any two elements with different units digits are in the right relative order. When I then sort on the tens digit, elements with the same tens digit need to retain their previous relative order (established by the units digit pass), which stability guarantees. By induction, after processing digit $k$, the array is correctly sorted considering only the last $k$ digits. Once $k$ equals the number of digits in the maximum element, the whole array is sorted.
Mathematical Foundation
If I have $n$ integers, each with $d$ digits, and each digit can take on $b$ possible values (the radix/base), then Radix Sort makes $d$ passes, and each pass involves a Counting Sort operation over $n$ elements with $b$ possible digit values.
The time complexity of a single Counting Sort pass is:
$$ O(n + b) $$
Since I perform this $d$ times, my total running time is:
$$ T(n) = O(d \cdot (n + b)) $$
If $b = O(n)$ and $d$ is treated as a constant (which is common when sorting fixed-width integers), this simplifies to:
$$ T(n) = O(n) $$
A useful way I think about $d$ is in terms of the maximum key value $k$ and the chosen base $b$:
$$ d = \log_b(k+1) $$
So the more precise running time, expressed in terms of $k$ and $b$, is:
$$ T(n) = O\left((n + b) \cdot \log_b(k+1)\right) $$
This formula tells me something important: choosing the base $b$ is a tuning decision. A larger base reduces the number of passes $d$ but increases the memory and per-pass cost from a larger bucket count $b$. Knuth’s analysis shows the optimal choice is often around $b \approx n$, balancing these two forces.
Diagrams
Here is a Mermaid diagram showing the overall pass-by-pass flow of LSD Radix Sort:
flowchart TD
A[Unsorted Array] --> B[Find Maximum Element]
B --> C[Determine Number of Digits d]
C --> D[Set exp = 1]
D --> E[Counting Sort on digit at exp]
E --> F{exp * 10 less than or equal to max?}
F -- Yes --> G[exp = exp * 10]
G --> E
F -- No --> H[Array is Fully Sorted]And here is a diagram showing what happens inside a single Counting Sort digit-pass:
flowchart LR
A[Input Array] --> B[Count occurrences of each digit 0-9]
B --> C[Compute prefix sums of counts]
C --> D[Place elements into output array right to left]
D --> E[Copy output back into input array]Pseudocode
Here is the language-independent pseudocode I follow:
RADIX-SORT(A, n)
max = MAXIMUM(A, n)
exp = 1
while max / exp > 0
COUNTING-SORT-BY-DIGIT(A, n, exp)
exp = exp * 10
COUNTING-SORT-BY-DIGIT(A, n, exp)
output = new array of size n
count = new array of size 10, initialized to 0
for i = 0 to n - 1
digit = (A[i] / exp) mod 10
count[digit] = count[digit] + 1
for i = 1 to 9
count[i] = count[i] + count[i - 1]
for i = n - 1 down to 0
digit = (A[i] / exp) mod 10
output[count[digit] - 1] = A[i]
count[digit] = count[digit] - 1
copy output into A
Step-by-Step Example
Let me walk through a concrete example using the array:
$$ [170, 45, 75, 90, 802, 24, 2, 66] $$
Step 1 — Sort by units digit (exp = 1):
Digits: 0, 5, 5, 0, 2, 4, 2, 6
Result after this pass: $$ [170, 90, 802, 2, 24, 45, 75, 66] $$
Step 2 — Sort by tens digit (exp = 10):
Digits: 7, 9, 0, 0, 2, 4, 7, 6
Result after this pass: $$ [802, 2, 24, 45, 66, 170, 75, 90] $$
Step 3 — Sort by hundreds digit (exp = 100):
Digits: 8, 0, 0, 0, 0, 1, 0, 0
Result after this final pass: $$ [2, 24, 45, 66, 75, 90, 170, 802] $$
The array is now fully sorted, and this matches exactly what my tested C implementation produces.
Time Complexity
- Best Case: $O(d \cdot (n + b))$ — the algorithm always performs the same number of passes regardless of input arrangement, so there’s no early-exit advantage.
- Average Case: $O(d \cdot (n + b))$ — same reasoning; Radix Sort has no notion of “nearly sorted” input speeding things up.
- Worst Case: $O(d \cdot (n + b))$ — again identical, since the algorithm is data-independent in its control flow.
This uniformity is actually one of Radix Sort’s more interesting properties to me: unlike QuickSort, whose worst case diverges wildly from its average case, Radix Sort’s performance is remarkably predictable.
Space Complexity
I need auxiliary space for:
- The
outputarray of size $n$ in each Counting Sort pass. - The
countarray of size $b$ (10 in the decimal case).
So the space complexity is:
$$ O(n + b) $$
This is not in-place, which I consider one of its genuine downsides compared to something like Heap Sort.
Correctness Analysis
I rely on a simple inductive argument to convince myself Radix Sort is correct.
Claim: After processing the first $k$ least-significant digits, the array is sorted correctly with respect to those $k$ digits (i.e., as if those $k$ digits were the entire number).
Base case: After processing 0 digits, the claim holds trivially.
Inductive step: Assume the array is correctly sorted with respect to the least significant $k$ digits. When I sort by digit $k+1$ using a stable sort, elements with equal $(k+1)$-th digits retain their relative order from the previous step — which was already correct for the lower $k$ digits. Elements with different $(k+1)$-th digits get properly separated. Therefore, the array is now correctly sorted with respect to $k+1$ digits.
By induction, once $k$ equals $d$ (the maximum digit count), the array is fully and correctly sorted. The load-bearing requirement throughout this proof is stability — without it, the induction breaks at the very first step.
Advantages
- Achieves linear time complexity, $O(n)$, under practical constraints on $b$ and $d$.
- Performance is highly predictable — no worst-case blowup like comparison sorts can have.
- Works very well for fixed-width integers, especially in specialized hardware or database indexing contexts.
- Naturally stable, which is useful when sorting records by multiple keys (I can sort by secondary key first, then primary key).
Disadvantages
- Not in-place; requires $O(n + b)$ additional memory.
- Only works cleanly on data with a natural digit/character decomposition — not general comparable objects.
- Performance depends heavily on the number of digits $d$; for very large integers or floating-point numbers, this benefit can shrink.
- Choosing the base $b$ requires tuning; a poor choice can make the algorithm slower in practice than a well-implemented comparison sort.
Applications
I’ve seen Radix Sort used in a range of practical contexts:
- Sorting large datasets of fixed-length integers, such as employee IDs or postal codes.
- String sorting in specialized contexts (radix sort on characters), such as suffix array construction in string-processing algorithms.
- Database systems that need to sort large volumes of numeric keys quickly.
- Computer graphics pipelines, where sorting by fixed-precision keys (like depth values) is common.
- GPU-based sorting implementations, since Radix Sort parallelizes well due to its lack of data-dependent comparisons.
Implementation in C
Here is my full working implementation, which I compiled and tested:
#include <stdio.h>
#include <stdlib.h>
// Returns the maximum value in the array so we know how many digits to process
int getMax(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
// A stable Counting Sort that sorts arr[] according to the digit
// represented by exp (exp = 1 for units, 10 for tens, 100 for hundreds, etc.)
void countingSortByDigit(int arr[], int n, int exp) {
int output[n];
int count[10] = {0};
// Count occurrences of each digit
for (int i = 0; i < n; i++)
count[(arr[i] / exp) % 10]++;
// Turn counts into prefix sums (positions in output)
for (int i = 1; i < 10; i++)
count[i] += count[i - 1];
// Build the output array, iterating backwards to preserve stability
for (int i = n - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
// Copy the sorted output back into the original array
for (int i = 0; i < n; i++)
arr[i] = output[i];
}
// Main Radix Sort driver: repeatedly sorts by each digit from LSD to MSD
void radixSort(int arr[], int n) {
int max = getMax(arr, n);
// exp represents the digit position: 1, 10, 100, ...
for (int exp = 1; max / exp > 0; exp *= 10)
countingSortByDigit(arr, n, exp);
}
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Before sorting: ");
printArray(arr, n);
radixSort(arr, n);
printf("After sorting: ");
printArray(arr, n);
return 0;
}
Sample Input and Output
Input:
170 45 75 90 802 24 2 66
Output (verified by compiling and running the code above):
Before sorting: 170 45 75 90 802 24 2 66
After sorting: 2 24 45 66 75 90 170 802
Optimization Techniques
A few techniques I keep in mind when I want to squeeze more performance out of Radix Sort:
- Choosing a larger base ($b$): Using base 256 (byte-wise processing) instead of base 10 reduces the number of passes for large integers, at the cost of a bigger count array per pass.
- In-place variants: Some implementations use clever swapping strategies to reduce auxiliary memory, though this typically sacrifices some stability guarantees or simplicity.
- Parallelization: Because each digit’s bucket placement is independent of comparisons, Radix Sort parallelizes well — this is why I often see it used in GPU sorting libraries.
- Hybrid with insertion sort: For very small subarrays or nearly-sorted data, switching to Insertion Sort as a final cleanup pass can sometimes reduce constant-factor overhead.
- Early termination: If I know the maximum number of digits in advance, I can skip the max-finding step, saving an $O(n)$ pass.
Common Mistakes
Mistakes I’ve seen (and made) while implementing this algorithm:
- Using an unstable sort within each digit pass. This is the single most common bug — it silently breaks correctness without throwing any errors.
- Forgetting to iterate backwards in the placement loop of Counting Sort, which also breaks stability.
- Off-by-one errors in prefix sums, especially when converting counts into position indices.
- Not handling negative numbers. Standard Radix Sort assumes non-negative integers; negative numbers require special handling (like separating negatives and positives, or applying an offset).
- Recomputing the maximum incorrectly or using the wrong digit count, leading to a premature stop before the most significant digit is processed.
Further Reading
- Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms (CLRS), MIT Press — https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
- Donald E. Knuth, The Art of Computer Programming, Volume 3: Sorting and Searching — https://www-cs-faculty.stanford.edu/~knuth/taocp.html
- GeeksforGeeks, “Radix Sort” — https://www.geeksforgeeks.org/dsa/radix-sort/
- Wikipedia, “Radix sort” — https://en.wikipedia.org/wiki/Radix_sort
- Visualgo, Sorting Visualization — https://visualgo.net/en/sorting
