Counting and Radix Sort
Sort small whole numbers in O(n) by counting instead of comparing, then extend it digit by digit with radix sort. Ends with a table of every sort.
On this page
You have 300 quiz papers, each scored from 0 to 10, and you need them in order. You could merge sort them. Or you could put 11 empty trays on the desk, labelled 0 to 10, drop each paper into the tray for its score, and then stack the trays in order. One look per paper, and no paper is ever compared with another.
That is counting sort. It beats every sort from the earlier lessons, but only because the scores are small whole numbers in a known range.
The O(n log n) wall
Merge sort, quicksort and heap sort all learn about the order by asking one question: is this item smaller than that one? Sorts that only ask that question can never do better than O(n log n) in the worst case.
Here is why. n items can be arranged in n × (n − 1) × … × 1 different orders, and the sort has to tell them all apart. Each comparison has two answers, so at best it cuts the possible orders in half. Halving that many orders down to one takes about n log₂ n comparisons. For 10 items there are 3,628,800 orders, and halving down to 1 takes at least 22 comparisons.
Counting sort steps around this wall: it never compares two items, and uses each key as an array index instead.
The idea: count, then place
Say every key is a whole number from 0 to k − 1.
- Count how many items have each key.
- Work out where each key starts in the output. Keys of 0 start at position 0. Keys of 1 start right after all the 0s, keys of 2 right after all the 0s and 1s, and so on. That is a running total of the counts, a prefix sum.
- Place each item, in input order, at its key's start position, and move that start position one step forward.
Step 3 walks the input from front to back, so items with equal keys land in the order they came in. So counting sort is stable, which radix sort will need.
In the trace below, the keys are 0 to 3, and the letter tells equal keys apart: 3a and 3b both have key 3, and 3a came first.
for each item: count[key] += 1start[0] = 0for v from 1 to k - 1: start[v] = start[v - 1] + count[v - 1]for each item, in input order: out[start[key]] = item start[key] += 1- input03a11a23b30a42a51bcount00102030
1Six items with keys between 0 and 3, so
k = 4. Start with four counters at zero. - input03a11a23b30a42a51bcount01122132
2One pass over the input. One item has key 0, two have key 1, one has key 2, two have key 3.
- input03a11a23b30a42a51bstart00112334out012345
3Running totals give the start of each key. No key is below 0, so 0s start at 0. One item has a key below 1, so 1s start at 1. Three items have keys below 2, so 2s start at 3. Four are below 3, so 3s start at 4.
- input03ax11a23b30a42a51bstart00112335out012343a5
4
3ahas key 3, which starts at 4. Put it atout[4], and move the start for 3 to 5. - input03a11ax23b30a42a51bstart00122335out011a2343a5
5
1agoes toout[1]. The start for 1 moves to 2. - input03a11a23bx30a42a51bstart00122336out011a2343a53b
6
3bgoes toout[5], right after3a, because the start for 3 already moved past it. - input03a11a23b30ax42a51bstart01122336out00a11a2343a53b
7
0agoes toout[0]. - input03a11a23b30a42ax51bstart01122436out00a11a232a43a53b
8
2agoes toout[3]. - input03a11a23b30a42a51bxstart01132436out00a11a21b32a43a53b
9
1bgoes toout[2], right after1a. - out00a11a21b32a43a53b
- comparisons
- 0
10Sorted, with
1astill before1band3abefore3b. Two passes over 6 items and one over 4 counters. Zero comparisons between items.
The code takes a key function that turns an item into its whole-number key, so it can sort records, not only bare numbers. Radix sort reuses it below.
// key(item) must be a whole number from 0 to k - 1
function countingSortBy(items, key, k) {
const count = new Array(k).fill(0);
for (const item of items) count[key(item)]++;
const start = new Array(k).fill(0); // start[v] = items with key < v
for (let v = 1; v < k; v++) start[v] = start[v - 1] + count[v - 1];
const out = new Array(items.length);
for (const item of items) {
out[start[key(item)]++] = item; // input order: stable
}
return out;
}
countingSortBy([3, 1, 3, 0, 2, 1], (x) => x, 4); // [0, 1, 1, 2, 3, 3]# key(item) must be a whole number from 0 to k - 1
def counting_sort_by(items, key, k):
count = [0] * k
for item in items:
count[key(item)] += 1
start = [0] * k # start[v] = items with key < v
for v in range(1, k):
start[v] = start[v - 1] + count[v - 1]
out = [None] * len(items)
for item in items: # input order: stable
out[start[key(item)]] = item
start[key(item)] += 1
return out
counting_sort_by([3, 1, 3, 0, 2, 1], lambda x: x, 4) # [0, 1, 1, 2, 3, 3]Radix sort: one digit at a time
Counting sort needs one counter per possible key. For quiz scores that is 11 counters. For order numbers up to a billion, it would be a billion counters, which is hopeless.
Radix sort fixes that by sorting on one digit at a time. Each digit is 0 to 9, so each pass is a counting sort with just 10 counters. The surprising part is the order: start with the last digit (the ones), then the tens, then the hundreds.
- nums0512103824073223409553406018
1Seven numbers, padded to three digits so every digit position is visible. The largest is 512, so three passes are enough.
- by ones0340151222233095440750386018
2Pass 1: stable sort by the ones digit. 340 ends in 0, so it goes first. 038 and 018 both end in 8 and keep their input order.
- by tens0407151220183223403853406095
3Pass 2: stable sort by the tens digit. 512 and 018 both have a 1 there. 512 stays first because pass 1 put it first, and its ones digit, 2, is smaller than 8.
- by tens0407151220183223403853406095
4Look at the last two digits now: 07, 12, 18, 23, 38, 40, 95. They are in order. After pass
p, the numbers are sorted by their lastpdigits. - by hundreds0018103820953223434054076512
5Pass 3: stable sort by the hundreds digit. 018, 038 and 095 all have 0 there, so stability keeps the order the earlier passes gave them.
- sorted0018103820953223434054076512
- passes
- 3
- counters per pass
- 10
6Fully sorted after 3 passes of 7 items each, and every pass used only 10 counters.
Here is why it works. The last pass decides by the most important digit. When two numbers tie on that digit, stability leaves them in the order the earlier passes chose, and that order was already correct for the lower digits. Each pass keeps all the work of the passes before it.
// non-negative integers; uses countingSortBy from above
function radixSort(nums) {
const biggest = nums.reduce((m, x) => Math.max(m, x), 0);
for (let place = 1; place <= biggest; place *= 10) {
nums = countingSortBy(nums, (x) => Math.floor(x / place) % 10, 10);
}
return nums;
}
radixSort([512, 38, 407, 223, 95, 340, 18]);
// [18, 38, 95, 223, 340, 407, 512]# non-negative integers; uses counting_sort_by from above
def radix_sort(nums):
biggest = max(nums, default=0)
place = 1
while place <= biggest:
nums = counting_sort_by(nums, lambda x: x // place % 10, 10)
place *= 10
return nums
radix_sort([512, 38, 407, 223, 95, 340, 18])
# [18, 38, 95, 223, 340, 407, 512]| Approach | Time | Space | Why |
|---|---|---|---|
| Counting sort | O(n + k) | O(n + k) | Passes over the n items and over the k counters. Great when k is not much bigger than n. |
| Radix sort | O(d × (n + b)) | O(n + b) | d digits, each a counting sort with b counters. Base 10 means b = 10. |
Every sort in this module
Here n is the number of items, k the number of possible keys, d the number of digits and b the base.
| Sort | Best | Average | Worst | Extra space | Stable | Reach for it when |
|---|---|---|---|---|---|---|
| Insertion | O(n) | O(n²) | O(n²) | O(1) | Yes | the input is tiny or nearly sorted |
| Selection | O(n²) | O(n²) | O(n²) | O(1) | No | writes are very expensive (at most n − 1 swaps) |
| Bubble | O(n) | O(n²) | O(n²) | O(1) | Yes | almost never; it is a teaching example |
| Merge | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | you need stability, a linked list, or data on disk |
| Quicksort | O(n log n) | O(n log n) | O(n²) | O(log n), O(n) worst | No | fast in-place sorting with a random pivot |
| Heap | O(n log n) | O(n log n) | O(n log n) | O(1) | No | you need a guarantee with no extra memory |
| Counting | O(n + k) | O(n + k) | O(n + k) | O(n + k) | Yes | keys are small whole numbers |
| Radix | O(d(n + b)) | O(d(n + b)) | O(d(n + b)) | O(n + b) | Yes | keys are fixed-length numbers or strings |
| Built-in (Timsort) | O(n) | O(n log n) | O(n log n) | O(n) | Yes | almost always, in real code |
Best cases assume distinct keys. With all keys equal, heap sort takes O(n) and the quicksort from its lesson takes O(n²).
In real code, call the built-in sort unless the right-hand column gives you a reason not to.
Common mistakes
Check yourself
Counts for keys 0 to 3 are [2, 0, 3, 1]. At which output position does the first item with key 3 go?
Radix sort on [21, 12, 22, 11]. What is the order after the ones pass?
You must sort 1 million user IDs, each a random number up to 1018. Is counting sort a good fit?
What to practise next
Sorting is often a first step. Once data is sorted, you can find anything in it in O(log n) with Binary Search, the next lesson.