Counting sort
Counting, radix and bucket sort avoid comparing elements with each other. They use the key values themselves as array indices, which lets them beat the Ω(n log n) comparison bound when the keys have a limited range or structure.
Learning goals
- Recognise when the key domain is small enough for counting sort
- Compute frequency and prefix counts and place elements stably
- Handle negative keys with an offset
- State the time and space of counting sort
Prerequisites
Sorting exam scores out of 100 for a million students does not need a comparison sort. Tally how many students got each score from 0 to 100, then write the scores out in order.
Bounded integer domain
Counting sort works when keys are integers in a known, small range [min, max] of size k = max - min + 1. It stores one counter per possible key, so it needs O(k) extra memory.
That is only a good deal when k is not much larger than n. Sorting 10 numbers between 1 and 1,000,000,000 would need a billion counters. Try the “Big gap (wasteful)” preset: five values need 19 counters.
Frequency and prefix counts
Pass 1 counts frequencies: count[x - min]++ for each element. Pass 2 turns the frequencies into prefix sums, count[v] += count[v - 1]. After that, count[v] is the number of elements with key <= v, which is also the index one past where the last copy of key v belongs in the output.
Pass 3 places elements. Walk the input right to left, decrement count[key], and write the element at that index. Watch the count row change from frequencies to prefix sums and then shrink while the output row fills.
Best
O(n + k)
Average
O(n + k)
Worst
O(n + k)
Space
O(n + k)
Counting sort
Interactive · edit the inputint[] arr · length 7
count[]
Keys lie in [1, 8], so 8 counters are enough. The offset x - min maps key 1 to counter 0.
1public static int[] countingSort(int[] arr) {2int min = min(arr), max = max(arr);3int[] count = new int[max - min + 1];4for (int x : arr) count[x - min]++;5for (int v = 1; v < count.length; v++) count[v] += count[v - 1];6int[] out = new int[arr.length];7for (int i = arr.length - 1; i >= 0; i--) {8out[--count[arr[i] - min]] = arr[i];9}10return out;11}
Live state
min
1
max
8
range
8
Execution trace (dry run table)Show 25 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | min=1 max=8 range=8 | Keys lie in [1, 8], so 8 counters are enough. The offset x - min maps key 1 to counter 0. |
| 2 | L4 | min=1 max=8 x=4 index=3 | count[4 - 1]++ → 1. Key 4 has been seen 1 time. |
| 3 | L4 | min=1 max=8 x=2 index=1 | count[2 - 1]++ → 1. Key 2 has been seen 1 time. |
| 4 | L4 | min=1 max=8 x=2 index=1 | count[2 - 1]++ → 2. Key 2 has been seen 2 times. |
| 5 | L4 | min=1 max=8 x=8 index=7 | count[8 - 1]++ → 1. Key 8 has been seen 1 time. |
| 6 | L4 | min=1 max=8 x=3 index=2 | count[3 - 1]++ → 1. Key 3 has been seen 1 time. |
| 7 | L4 | min=1 max=8 x=3 index=2 | count[3 - 1]++ → 2. Key 3 has been seen 2 times. |
| 8 | L4 | min=1 max=8 x=1 index=0 | count[1 - 1]++ → 1. Key 1 has been seen 1 time. |
| 9 | L4 | min=1 max=8 | Counting pass done: count[v] is how many elements have that key. |
| 10 | L5 | min=1 max=8 v=2 | count[1] += count[0] → 3. Now count[v] is the number of elements with key <= 2, which is where key 2 ends in the output. |
| 11 | L5 | min=1 max=8 v=3 | count[2] += count[1] → 5. Now count[v] is the number of elements with key <= 3, which is where key 3 ends in the output. |
| 12 | L5 | min=1 max=8 v=4 | count[3] += count[2] → 6. Now count[v] is the number of elements with key <= 4, which is where key 4 ends in the output. |
| 13 | L5 | min=1 max=8 v=5 | count[4] += count[3] → 6. Now count[v] is the number of elements with key <= 5, which is where key 5 ends in the output. |
| 14 | L5 | min=1 max=8 v=6 | count[5] += count[4] → 6. Now count[v] is the number of elements with key <= 6, which is where key 6 ends in the output. |
| 15 | L5 | min=1 max=8 v=7 | count[6] += count[5] → 6. Now count[v] is the number of elements with key <= 7, which is where key 7 ends in the output. |
| 16 | L5 | min=1 max=8 v=8 | count[7] += count[6] → 7. Now count[v] is the number of elements with key <= 8, which is where key 8 ends in the output. |
| 17 | L6 | min=1 max=8 | Allocate out[]. Walk the input right to left so equal keys keep their original order. |
| 18 | L8 | min=1 max=8 x=1 --count=0 slot=0 | --count[0] = 0, so out[0] = 1. Moving right to left puts the last copy of 1 at the end of its block. |
| 19 | L8 | min=1 max=8 x=3 --count=4 slot=4 | --count[2] = 4, so out[4] = 3. Moving right to left puts the last copy of 3 at the end of its block. |
| 20 | L8 | min=1 max=8 x=3 --count=3 slot=3 | --count[2] = 3, so out[3] = 3. Moving right to left puts the last copy of 3 at the end of its block. |
| 21 | L8 | min=1 max=8 x=8 --count=6 slot=6 | --count[7] = 6, so out[6] = 8. Moving right to left puts the last copy of 8 at the end of its block. |
| 22 | L8 | min=1 max=8 x=2 --count=2 slot=2 | --count[1] = 2, so out[2] = 2. Moving right to left puts the last copy of 2 at the end of its block. |
| 23 | L8 | min=1 max=8 x=2 --count=1 slot=1 | --count[1] = 1, so out[1] = 2. Moving right to left puts the last copy of 2 at the end of its block. |
| 24 | L8 | min=1 max=8 x=4 --count=5 slot=5 | --count[3] = 5, so out[5] = 4. Moving right to left puts the last copy of 4 at the end of its block. |
| 25 | L10 | Sorted in 22 steps of O(n + k) work, with no comparisons between elements. The a/b tags on equal keys show the sort is stable. |
Stable output
Walking the input from right to left while decrementing the counter places the last of several equal keys in the last slot of its block, the next-to-last in the slot before it, and so on. Equal keys therefore stay in their original order.
Stability is what makes counting sort usable as the digit pass of radix sort in the next lesson. Walking left to right would still sort the keys correctly but would reverse the order of equal keys. Run the “Stability check” preset and read the a/b tags.
1public static int[] countingSort(int[] arr) {2int min = Arrays.stream(arr).min().getAsInt();3int max = Arrays.stream(arr).max().getAsInt();4int[] count = new int[max - min + 1];5for (int x : arr) count[x - min]++;6for (int v = 1; v < count.length; v++) count[v] += count[v - 1];7int[] out = new int[arr.length];8for (int i = arr.length - 1; i >= 0; i--) { // right to left keeps it stable9out[--count[arr[i] - min]] = arr[i];10}11return out;12}
Negative keys offset
Array indices cannot be negative, so subtract the minimum key: x - min. The smallest key maps to index 0 and the largest to max - min. Add min back when you need the key value from a counter index.
The “Negative keys” preset sorts values between -4 and 5 with ten counters. If you accidentally use x as the index, a key of -2 throws ArrayIndexOutOfBoundsException.
Complexity
Time: one pass over the input to count (n), one over the counters for the prefix sums (k), one over the input to place (n): O(n + k). Space: the counter array plus the output array, O(n + k).
When k = O(n) this is linear, beating the Ω(n log n) lower bound for comparison sorts. There is no contradiction: counting sort never compares two elements, so the bound does not apply to it.
| Algorithm | Time | Extra space | Stable | Needs |
|---|---|---|---|---|
| Counting sort | O(n + k) | O(n + k) | Yes | Integer keys in a small range k |
| Merge sort | O(n log n) | O(n) | Yes | A comparator |
| Quick sort | O(n log n) avg | O(log n) | No | A comparator |
Best
O(n + k)
Average
O(n + k)
Worst
O(n + k)
Space
O(n + k)
Practice ladder
Now try it yourself
Easy
- Sort an array of exam scores 0-100
- Sort a string of lowercase letters with 26 counters
- Find the most frequent element using counts
Medium
- Sort records by an integer age field, stably
- Sort an array with negative numbers using an offset
- Find the h-index using counting sort
Advanced
- Sort strings by length in O(total characters)
- Relative sort array: order by another array, then ascending
- In-place counting sort variant and discuss why it loses stability
Key takeaways
- Counting sort is O(n + k) time and space for integer keys in a range of size k.
- Prefix sums turn counts into output positions; a right-to-left pass makes it stable.
- Subtract the minimum key to handle negatives; avoid it when k is huge compared to n.