When I first encountered Counting Sort, what struck me most was how unlike a traditional sorting algorithm it felt. I wasn’t comparing elements to each other at all — I was counting them. That single shift in perspective is what lets Counting Sort achieve linear time, something no comparison-based algorithm can guarantee in the general case.
I think of Counting Sort as the conceptual foundation beneath Radix Sort — in fact, I use it as the subroutine inside every digit pass of Radix Sort in my other article. Understanding it well pays off twice.
History and Background
Counting Sort’s origins are tied closely to the same era of computing history as Radix Sort. I found that Harold H. Seward is generally credited with formally describing Counting Sort in 1954, in the same MIT thesis work where he described Radix Sort. The idea of tallying occurrences of values before placing them into position has an even older intuitive lineage in manual data-processing and tabulation techniques, but Seward’s formalization is what modern textbooks (including Knuth’s The Art of Computer Programming) trace it back to.
I find it fitting that Counting Sort and Radix Sort share a birthplace — they are, in a real sense, two halves of the same idea: one handles a single digit or small-range key perfectly, and the other repeats that process across multiple digit positions to handle arbitrarily large numbers.
Problem Statement
The problem I’m solving is: given an array of $n$ integers, each falling within a known and reasonably small range $[min, max]$, arrange them in sorted order.
The key constraint that makes Counting Sort applicable is that the range of values, $k = max – min + 1$, must be small enough relative to $n$ that allocating an array of size $k$ is practical. If $k$ is enormous compared to $n$, Counting Sort loses its advantage and a comparison-based sort becomes more practical.
Core Concepts
- Key range ($k$): The number of distinct possible values the elements can take, computed as $max – min + 1$.
- Count array: An auxiliary array of size $k$ where each index stores how many times the corresponding value appears in the input.
- Prefix sum (cumulative count): Converting the count array into cumulative counts, so that
count[i]tells me how many elements are less than or equal to value $i$. - Stability: Counting Sort, when implemented carefully (iterating the input array backwards during placement), preserves the relative order of equal elements — a property I rely on heavily when using it inside Radix Sort.
- Non-comparison sort: A sort that determines ordering through counting/indexing arithmetic rather than pairwise comparisons.
How It Works
Here is the sequence of steps I follow:
- Find the minimum and maximum values in the input array to determine the range $k$.
- Create a count array of size $k$, initialized to all zeros.
- Iterate through the input array once, incrementing
count[value - min]for each element. - Transform the count array into a cumulative (prefix sum) array, where each entry represents how many elements are less than or equal to that value.
- Iterate through the input array from right to left, placing each element into its correct position in an output array (using the cumulative count to determine position), then decrementing the count for that value.
- Copy the output array back into the original array (or return it directly).
Working Principle
The internal mechanism relies on the idea that if I know how many elements are less than or equal to a given value, I know exactly where that value’s elements belong in the final sorted array.
For instance, if count[5] = 7 after the prefix sum step, that tells me there are 7 elements in the array with value less than or equal to 5, meaning the last occurrence of value 5 belongs at index 6 (0-indexed) in the sorted output. As I place elements one at a time, I decrement the count so subsequent occurrences of the same value slot in just before the previous one — which is exactly why I must iterate backwards through the input to preserve stability.
Mathematical Foundation
Given $n$ elements and a key range of $k$ distinct values, Counting Sort performs:
- One pass of size $n$ to build the initial counts: $O(n)$
- One pass of size $k$ to compute prefix sums: $O(k)$
- One pass of size $n$ (backwards) to place elements: $O(n)$
- One pass of size $n$ to copy the output back: $O(n)$
Adding these together:
$$ T(n, k) = O(n + k) $$
This is the defining formula of Counting Sort. If $k = O(n)$, then:
$$ T(n, k) = O(n) $$
which is strictly better than the $\Omega(n \log n)$ lower bound that applies to comparison-based sorting — because Counting Sort isn’t a comparison sort at all, it isn’t bound by that theorem.
A quick sanity check I like to do: the total number of elements placed into the output array must equal $n$, and this is guaranteed because:
$$ \sum_{i=0}^{k-1} \text{count}[i] = n $$
which holds by construction, since every element increments exactly one count bucket.
Diagrams
Here’s a high-level flow of Counting Sort:
flowchart TD
A[Input Array] --> B[Find Min and Max Values]
B --> C[Create Count Array of size k]
C --> D[Count Occurrences of Each Value]
D --> E[Compute Prefix Sums]
E --> F[Place Elements into Output Array Right to Left]
F --> G[Copy Output Back to Original Array]And a diagram illustrating how the prefix sum step converts raw counts into positions:
flowchart LR
A["Raw counts: value -> frequency"] --> B["Cumulative sum: count[i] += count[i-1]"]
B --> C["count[i] now means: elements <= i"]
C --> D["Used as insertion index for placement"]
Pseudocode
COUNTING-SORT(A, n)
min = MINIMUM(A, n)
max = MAXIMUM(A, n)
k = max - min + 1
count = new array of size k, initialized to 0
output = new array of size n
for i = 0 to n - 1
count[A[i] - min] = count[A[i] - min] + 1
for i = 1 to k - 1
count[i] = count[i] + count[i - 1]
for i = n - 1 down to 0
output[count[A[i] - min] - 1] = A[i]
count[A[i] - min] = count[A[i] - min] - 1
copy output into A
Step-by-Step Example
Let me trace through the array:
$$ [4, 2, 2, 8, 3, 3, 1, 0, 5] $$
Step 1 — Find range: min = 0, max = 8, so $k = 9$.
Step 2 — Build raw counts (index = value, since min is 0):
| Value | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Count | 1 | 1 | 2 | 2 | 1 | 1 | 0 | 0 | 1 |
Step 3 — Convert to prefix sums:
| Value | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Count | 1 | 2 | 4 | 6 | 7 | 8 | 8 | 8 | 9 |
Step 4 — Place elements, iterating input backwards:
Processing order: 5, 0, 1, 3, 3, 8, 2, 2, 4
Each placement uses the current count value as the (1-indexed) position, then decrements it. Working through all nine elements this way produces:
$$ [0, 1, 2, 2, 3, 3, 4, 5, 8] $$
This matches exactly what my tested C implementation outputs.
Time Complexity
- Best Case: $O(n + k)$
- Average Case: $O(n + k)$
- Worst Case: $O(n + k)$
I want to emphasize something here: Counting Sort has no notion of “bad input” the way QuickSort does. Its performance depends entirely on $n$ and $k$, never on the arrangement of the data. That predictability is one of the things I appreciate most about it.
Space Complexity
I need:
- A count array of size $k$: $O(k)$
- An output array of size $n$: $O(n)$
Total auxiliary space:
$$ O(n + k) $$
This is a real cost. If $k$ is very large relative to $n$ — say, sorting 100 integers where values range from 0 to 10 million — Counting Sort becomes impractical despite its linear-time reputation.
Correctness Analysis
I convince myself of correctness through the prefix sum invariant. After the cumulative sum step, count[v] represents exactly the number of elements in the input with value $\leq v$. This means the last occurrence of value $v$, when placed, must go to index count[v] - 1 in the (0-indexed) output array.
By iterating the input backwards and decrementing count[v] after each placement of value $v$, I guarantee:
- Every element lands in a position consistent with how many elements are less than or equal to it.
- Multiple elements with the same value get placed in consecutive positions, in the same relative order they appeared in the input (this is the stability property — the last occurrence found while scanning backwards is placed first, at the highest available slot, then earlier occurrences fill in below it, preserving original order).
Since every element is placed exactly once, and no two elements can be placed in the same output slot (each placement decrements the shared counter), the output array is a valid permutation of the input, and it is sorted by construction.
Advantages
- True linear time, $O(n + k)$, which beats the comparison-sort lower bound entirely.
- Naturally stable when implemented with a backward iteration during placement.
- Simple to implement correctly compared to many comparison-based sorts.
- Serves as a building block for more general algorithms, like Radix Sort.
Disadvantages
- Requires knowledge of the key range in advance (or an extra pass to compute it).
- Impractical when the range $k$ is much larger than $n$, since memory and time scale with $k$.
- Only works for discrete values that can be mapped to array indices (integers, or things reducible to integers) — not arbitrary comparable objects.
- Not in-place; requires $O(n+k)$ additional memory.
Applications
- As the core subroutine inside Radix Sort, applied once per digit.
- Sorting exam scores, ages, or other bounded-range numeric data.
- Histogram-based algorithms in image processing, where pixel intensity values fall within a small fixed range (like 0–255).
- Bucketing and frequency-analysis tasks in data preprocessing pipelines.
- Rank computation — figuring out an element’s position within a distribution without full sorting overhead.
Implementation in C
Here is my tested implementation, which handles arbitrary min/max ranges (not just non-negative values starting at zero):
#include <stdio.h>
#include <stdlib.h>
// Sorts arr[] in place using Counting Sort.
// Handles negative numbers by shifting all values relative to the minimum.
void countingSort(int arr[], int n) {
if (n == 0) return;
// Step 1: find the range of values
int max = arr[0], min = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) max = arr[i];
if (arr[i] < min) min = arr[i];
}
int range = max - min + 1;
int *count = calloc(range, sizeof(int)); // zero-initialized
int *output = malloc(n * sizeof(int));
// Step 2: count occurrences of each value (shifted by min)
for (int i = 0; i < n; i++)
count[arr[i] - min]++;
// Step 3: convert counts into prefix sums (positions)
for (int i = 1; i < range; i++)
count[i] += count[i - 1];
// Step 4: place elements into output, iterating backwards for stability
for (int i = n - 1; i >= 0; i--) {
output[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}
// Step 5: copy the sorted output back into the original array
for (int i = 0; i < n; i++)
arr[i] = output[i];
free(count);
free(output);
}
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr[] = {4, 2, 2, 8, 3, 3, 1, 0, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Before sorting: ");
printArray(arr, n);
countingSort(arr, n);
printf("After sorting: ");
printArray(arr, n);
return 0;
}
Sample Input and Output
Input:
4 2 2 8 3 3 1 0 5
Output (verified by compiling and running the code above):
Before sorting: 4 2 2 8 3 3 1 0 5
After sorting: 0 1 2 2 3 3 4 5 8
Optimization Techniques
- Avoid recomputing min/max repeatedly if sorting the same dataset multiple times or in streaming contexts — cache the range when possible.
- In-place variants exist that avoid the separate output array by using swaps, though they generally sacrifice stability.
- Combine with Radix Sort when the range $k$ is too large for direct Counting Sort but the values can be decomposed into digits, giving me the benefit of linear time without the memory blowup.
- Parallel counting: the counting phase (Step 2) can be parallelized across multiple threads with a final merge/reduction step, since each element’s contribution to the count array is independent.
- Use unsigned types carefully when shifting negative values, to avoid overflow issues on very large ranges.
Common Mistakes
- Forgetting to shift by the minimum value, which causes negative array indices and undefined behavior for input containing negative numbers.
- Iterating forwards instead of backwards during the placement step, which silently breaks stability without causing a visible error.
- Off-by-one indexing errors when converting counts into positions — remembering that
count[v] - 1(notcount[v]) is the correct index. - Not validating that $k$ is reasonably bounded, leading to excessive memory allocation or even allocation failures for very large ranges.
- Assuming Counting Sort works on floating-point or non-integer data without first mapping those values to a discrete index space.
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, “Counting Sort” — https://www.geeksforgeeks.org/dsa/counting-sort/
- Wikipedia, “Counting sort” — https://en.wikipedia.org/wiki/Counting_sort
- Visualgo, Sorting Visualization — https://visualgo.net/en/sorting
