Count Frequencies Instead of Comparing — O(n + k) When the Key Range Is Bounded
Counting Sort
Counting sort sidesteps the Ω(n log n) comparison-sort lower bound by tallying how many times each value appears and computing positions directly — blazing fast when the value range k is small relative to n.
What you'll learn
- Implement a stable counting sort for non-negative integer arrays
- Explain the prefix-sum technique that makes counting sort stable
- Identify when counting sort is and is not appropriate
Counting sort belongs to a family of non-comparison sorts. Instead of asking “is A less than B?”, it counts how many elements equal each possible value, then uses those counts to place each element directly into its correct output position. Because it never compares two elements, it is not subject to the Ω(n log n) lower bound.
The Three-Step Algorithm
- Count — create an array
countof length k + 1 (where k is the maximum value). Incrementcount[v]for each element with value v. - Prefix sum — replace each
count[i]with the sum of all counts up to and including i. Nowcount[v]holds the number of elements with value ≤ v, which is exactly the index (1-based) of the last occurrence of v in the output. - Place — iterate the input from right to left. For each element v,
place it at output position
count[v] - 1, then decrementcount[v]. Right-to-left traversal is what makes the sort stable.
JavaScript Implementation
function countingSort(arr, maxVal) {
const n = arr.length;
const count = new Array(maxVal + 1).fill(0);
const output = new Array(n);
// Step 1: count occurrences
for (let i = 0; i < n; i++) {
count[arr[i]]++;
}
// Step 2: prefix sums — count[i] now holds number of elements <= i
for (let i = 1; i <= maxVal; i++) {
count[i] += count[i - 1];
}
// Step 3: place elements right-to-left to preserve stability
for (let i = n - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
return output;
}
console.log(countingSort([4, 2, 2, 8, 3, 3, 1], 8));
// => [1, 2, 2, 3, 3, 4, 8] Stability Explained
The right-to-left traversal in Step 3 ensures that among elements with the same value, the one that appeared last in the input is placed last in the output — preserving their original relative order. This stability property is what makes counting sort suitable as the inner sort in radix sort.
Complexity
| Case | Time | Space | Stable | In-Place |
|---|---|---|---|---|
| Best | O(n + k) | O(n+k) | Yes | No |
| Average | O(n + k) | O(n+k) | Yes | No |
| Worst | O(n + k) | O(n+k) | Yes | No |
Here k is the range of input values (maxVal − minVal + 1).
When Counting Sort Wins
Counting sort is ideal when k is small relative to n — for example, sorting exam scores (0–100) for a class of 1,000 students, or sorting single bytes in a radix sort pass. It is the wrong choice when k is large (sorting 32-bit integers with k ≈ 4 billion would need 4 GB of count space) or when elements are not non-negative integers.
Handling Negative Numbers
Subtract the minimum value from every element before sorting and add it back afterward. This shifts the range to start at 0 without changing the relative order.
Up Next
Radix sort extends the counting-sort idea to multi-digit numbers, sorting digit by digit from least significant to most significant.
Radix Sort →