Chapter 18: Efficient and Specialized Sorting
18.04Beginner / Intermediate5 topics14 min1 interactive animation

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

ArraysPrefix sumsStable sorting

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.

18.04.1

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.

18.04.2

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 input

Your input

Try

int[] arr · length 7

4
2a
2b
8
3a
3b
1
0123456
Being readBeing placedIn outputaEqual keys in original order

count[]

01
02
03
04
05
06
07
08
Setup

Keys lie in [1, 8], so 8 counters are enough. The offset x - min maps key 1 to counter 0.

Step 1 / 25
Java
1public static int[] countingSort(int[] arr) {
2 int min = min(arr), max = max(arr);
3 int[] count = new int[max - min + 1];
4 for (int x : arr) count[x - min]++;
5 for (int v = 1; v < count.length; v++) count[v] += count[v - 1];
6 int[] out = new int[arr.length];
7 for (int i = arr.length - 1; i >= 0; i--) {
8 out[--count[arr[i] - min]] = arr[i];
9 }
10 return out;
11}

Live state

0 reads0 writes

min

1

max

8

range

8

No condition evaluated on this step
Execution trace (dry run table)Show 25 steps
StepLineStateWhat happens
1L2min=1 max=8 range=8Keys lie in [1, 8], so 8 counters are enough. The offset x - min maps key 1 to counter 0.
2L4min=1 max=8 x=4 index=3count[4 - 1]++ → 1. Key 4 has been seen 1 time.
3L4min=1 max=8 x=2 index=1count[2 - 1]++ → 1. Key 2 has been seen 1 time.
4L4min=1 max=8 x=2 index=1count[2 - 1]++ → 2. Key 2 has been seen 2 times.
5L4min=1 max=8 x=8 index=7count[8 - 1]++ → 1. Key 8 has been seen 1 time.
6L4min=1 max=8 x=3 index=2count[3 - 1]++ → 1. Key 3 has been seen 1 time.
7L4min=1 max=8 x=3 index=2count[3 - 1]++ → 2. Key 3 has been seen 2 times.
8L4min=1 max=8 x=1 index=0count[1 - 1]++ → 1. Key 1 has been seen 1 time.
9L4min=1 max=8Counting pass done: count[v] is how many elements have that key.
10L5min=1 max=8 v=2count[1] += count[0] → 3. Now count[v] is the number of elements with key <= 2, which is where key 2 ends in the output.
11L5min=1 max=8 v=3count[2] += count[1] → 5. Now count[v] is the number of elements with key <= 3, which is where key 3 ends in the output.
12L5min=1 max=8 v=4count[3] += count[2] → 6. Now count[v] is the number of elements with key <= 4, which is where key 4 ends in the output.
13L5min=1 max=8 v=5count[4] += count[3] → 6. Now count[v] is the number of elements with key <= 5, which is where key 5 ends in the output.
14L5min=1 max=8 v=6count[5] += count[4] → 6. Now count[v] is the number of elements with key <= 6, which is where key 6 ends in the output.
15L5min=1 max=8 v=7count[6] += count[5] → 6. Now count[v] is the number of elements with key <= 7, which is where key 7 ends in the output.
16L5min=1 max=8 v=8count[7] += count[6] → 7. Now count[v] is the number of elements with key <= 8, which is where key 8 ends in the output.
17L6min=1 max=8Allocate out[]. Walk the input right to left so equal keys keep their original order.
18L8min=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.
19L8min=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.
20L8min=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.
21L8min=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.
22L8min=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.
23L8min=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.
24L8min=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.
25L10Sorted 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.
18.04.3

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.

Java
1public static int[] countingSort(int[] arr) {
2 int min = Arrays.stream(arr).min().getAsInt();
3 int max = Arrays.stream(arr).max().getAsInt();
4 int[] count = new int[max - min + 1];
5 for (int x : arr) count[x - min]++;
6 for (int v = 1; v < count.length; v++) count[v] += count[v - 1];
7 int[] out = new int[arr.length];
8 for (int i = arr.length - 1; i >= 0; i--) { // right to left keeps it stable
9 out[--count[arr[i] - min]] = arr[i];
10 }
11 return out;
12}
18.04.4

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.

18.04.5

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.

AlgorithmTimeExtra spaceStableNeeds
Counting sortO(n + k)O(n + k)YesInteger keys in a small range k
Merge sortO(n log n)O(n)YesA comparator
Quick sortO(n log n) avgO(log n)NoA 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.