When learning sorting algorithms, we usually encounter algorithms such as:
Most of these algorithms compare elements with each other.
For example:
5 > 3
8 > 2
4 < 7This leads to an important theoretical limitation: comparison-based sorting algorithms cannot generally do better than Ω(n log n) in the comparison model.
But what if we don't compare the elements at all?
What if we use the values themselves as information to determine where elements belong?
That's exactly the idea behind Counting Sort.
Counting Sort can sort certain integer datasets in:
O(n + k)where:
n = number of elementsk = range of valuesWhen k is reasonably small, Counting Sort can be remarkably fast.
In this article, we'll understand Counting Sort from the ground up, implement it in Python, make it stable, handle negative numbers, understand prefix sums, and see why Counting Sort is an important building block for Radix Sort.
Counting Sort is a non-comparison-based sorting algorithm that sorts integers by counting how many times each value occurs.
Instead of asking:
Is
7greater than4?
Counting Sort asks:
How many
4s are there?
How many
5s are there?
How many
6s are there?
And so on.
Consider:
[4, 2, 2, 8, 3, 3, 1]We can count the occurrences:
1 → 1
2 → 2
3 → 2
4 → 1
5 → 0
6 → 0
7 → 0
8 → 1Now we simply reconstruct the array:
[1, 2, 2, 3, 3, 4, 8]No element-to-element comparisons were required.
Consider Quick Sort.
Quick Sort repeatedly performs comparisons:
5 < 8
2 < 5
7 > 5Merge Sort does the same:
3 < 8
4 > 2Counting Sort works differently.
For:
[4, 2, 2, 8, 3, 3, 1]it creates a frequency table.
Value: 1 2 3 4 5 6 7 8
Count: 1 2 2 1 0 0 0 1The counts directly tell us how many copies of each value should appear.
That's why Counting Sort is called a non-comparison sorting algorithm.
Counting Sort follows a simple process.
Find:
minimum value
maximum valueFor:
[4, 2, 2, 8, 3, 3, 1]we have:
min = 1
max = 8Therefore:
range = 8 - 1 + 1 = 8Create an array of size 8:
count = [0, 0, 0, 0, 0, 0, 0, 0]Process the input:
4
2
2
8
3
3
1The count array becomes:
Index: 0 1 2 3 4 5 6 7
Value: 1 2 3 4 5 6 7 8
Count: 1 2 2 1 0 0 0 1Read the count array from left to right.
1 → once
2 → twice
3 → twice
4 → once
8 → onceResult:
[1, 2, 2, 3, 3, 4, 8]That's Counting Sort.
Let's take:
arr = [4, 2, 2, 8, 3, 3, 1]4 2 2 8 3 3 11 → 1
2 → 2
3 → 2
4 → 1
5 → 0
6 → 0
7 → 0
8 → 11
2 2
3 3
4
8Final:
[1, 2, 2, 3, 3, 4, 8]Here is the simplest implementation in Python:
def counting_sort(arr):
if not arr:
return arr
min_value = min(arr)
max_value = max(arr)
count = [0] * (max_value - min_value + 1)
# Count occurrences
for num in arr:
count[num - min_value] += 1
# Reconstruct sorted array
index = 0
for i, frequency in enumerate(count):
value = i + min_value
for _ in range(frequency):
arr[index] = value
index += 1
return arr
arr = [4, 2, 2, 8, 3, 3, 1]
print(counting_sort(arr))Output:
[1, 2, 2, 3, 3, 4, 8]num - min_value?You may have noticed:
count[num - min_value] += 1Why not:
count[num] += 1Because the minimum value doesn't necessarily have to be 0.
Suppose:
arr = [10, 12, 11, 10]Then:
min = 10
max = 12The required range is:
10, 11, 12We only need three positions.
We map:
10 → 0
11 → 1
12 → 2using:
index = value - minTherefore:
10 - 10 = 0
11 - 10 = 1
12 - 10 = 2This makes the algorithm work efficiently even when the values don't start from zero.
Counting Sort can also handle negative integers if we use an offset.
Consider:
[-5, -2, -5, 0, 3, -1]Minimum:
-5Maximum:
3Range:
3 - (-5) + 1 = 9Mapping becomes:
-5 → 0
-4 → 1
-3 → 2
-2 → 3
-1 → 4
0 → 5
1 → 6
2 → 7
3 → 8The same formula works:
index = num - min_valueImplementation:
def counting_sort(arr):
if not arr:
return arr
min_value = min(arr)
max_value = max(arr)
count = [0] * (max_value - min_value + 1)
for num in arr:
count[num - min_value] += 1
index = 0
for i, frequency in enumerate(count):
value = i + min_value
for _ in range(frequency):
arr[index] = value
index += 1
return arr
arr = [-5, -2, -5, 0, 3, -1]
print(counting_sort(arr))Output:
[-5, -5, -2, -1, 0, 3]Counting Sort's performance depends heavily on the range of values.
Suppose:
arr = [1, 2, 3, 4, 5]Here:
n = 5
k = 5Excellent.
But consider:
arr = [1, 1000000000]There are only two elements.
n = 2But:
k = 1,000,000,000Creating:
[0, 0, 0, 0, 0, ...]with one billion positions would be extremely wasteful.
This gives us one of the most important rules for Counting Sort:
Counting Sort is efficient when the value range is not much larger than the number of elements.
Counting Sort has:
O(n + k)time complexity.
Where:
n = number of input elementsk = range of valuesWhy?
We perform operations involving:
ninput elements.
And we process:
kpossible values.
Therefore:
O(n + k)Potentially, yes.
Suppose:
n = 1,000,000
k = 1,000Counting Sort performs roughly:
O(1,001,000)scale operations.
A comparison sort would typically be around:
O(n log n)which is substantially larger.
But this does not mean Counting Sort is always faster.
If:
n = 1,000
k = 1,000,000,000Counting Sort becomes impractical.
So algorithm selection depends on the relationship between:
nand:
kThe basic version requires a count array of size k.
Therefore:
Space = O(k)If we use the stable version, we typically also create an output array of size n.
Then:
Space = O(n + k)The simple reconstruction version we wrote is not generally considered a stable sorting implementation because it doesn't preserve the relative ordering of equal elements when sorting records.
For example, imagine:
[
(2, "A"),
(1, "B"),
(2, "C")
]A stable sort must produce:
(1, "B")
(2, "A")
(2, "C")Notice:
Aremains before:
Cbecause both have key 2.
To achieve this, we use cumulative counts and an output array.
This is one of the most important versions of Counting Sort.
Suppose we have:
arr = [4, 2, 2, 8, 3, 3, 1]Frequency counts:
Value: 1 2 3 4 5 6 7 8
Count: 1 2 2 1 0 0 0 1Now calculate cumulative counts:
Value: 1 2 3 4 5 6 7 8
Frequency: 1 2 2 1 0 0 0 1
Cumulative: 1 3 5 6 6 6 6 7The cumulative count tells us the final position boundary of each value.
For example:
1 → position 1
2 → position 3
3 → position 5
4 → position 6
8 → position 7This lets us place every element into its correct position.
def counting_sort(arr):
if not arr:
return arr
min_value = min(arr)
max_value = max(arr)
k = max_value - min_value + 1
count = [0] * k
# Step 1: Count frequencies
for num in arr:
count[num - min_value] += 1
# Step 2: Convert counts to cumulative counts
for i in range(1, k):
count[i] += count[i - 1]
# Step 3: Build output array
output = [0] * len(arr)
for num in reversed(arr):
index = num - min_value
output[count[index] - 1] = num
count[index] -= 1
return output
arr = [4, 2, 2, 8, 3, 3, 1]
print(counting_sort(arr))Output:
[1, 2, 2, 3, 3, 4, 8]This line is critical:
for num in reversed(arr):Why?
Because when multiple elements have the same key, processing from right to left preserves their original relative order.
That is what gives us stability.
This technique becomes particularly important when Counting Sort is used inside Radix Sort.
Let's compare them.
| Feature | Counting Sort | Quick Sort |
|---|---|---|
| Type | Non-comparison | Comparison |
| Average Time | O(n + k) | O(n log n) |
| Worst Time | O(n + k) | O(n²) |
| Extra Space | O(k) | O(log n) average recursion |
| Stable | Yes, with stable implementation | Usually no |
| Works on general objects | No | Yes, with comparator |
| Best for | Small integer ranges | General-purpose sorting |
Counting Sort can outperform Quick Sort when:
k ≈ O(n)and the data consists of suitable integer keys.
| Feature | Counting Sort | Merge Sort |
|---|---|---|
| Comparison-based | No | Yes |
| Time | O(n + k) | O(n log n) |
| Stable | Yes, when implemented stably | Yes |
| Extra Space | O(n + k) stable version | O(n) |
| General data | Limited | Yes |
| Integer keys | Excellent | Good |
| Huge value range | Poor | Good |
Merge Sort is more general.
Counting Sort is more specialized.
| Feature | Counting Sort | Heap Sort |
|---|---|---|
| Comparison-based | No | Yes |
| Time | O(n + k) | O(n log n) |
| Stable | Stable version possible | No |
| Extra Space | O(k) / O(n+k) | O(1) |
| Integer range important | Yes | No |
| General purpose | No | Yes |
Counting Sort is useful when the possible values are limited.
Suppose students receive scores from:
0 to 100Even if there are:
1,000,000 studentsthe range is only:
101Counting Sort can be extremely effective.
If you're sorting people's ages:
0 to 120the range is tiny.
A frequency array is an obvious solution.
Suppose ratings are:
1, 2, 3, 4, 5Counting frequencies is very efficient.
Counting techniques are widely useful when processing:
letters
digits
small integer IDs
categorical valuesThere is a strong connection between Counting Sort and histograms.
A histogram answers:
How many times does each value occur?
Counting Sort uses exactly this information.
For example:
Data:
1 2 2 3 3 3 4 5Frequency:
1 → █
2 → ██
3 → ███
4 → █
5 → █This is essentially the same information required by Counting Sort.
Counting Sort becomes even more interesting when we move toward advanced algorithms.
Consider a number:
329We can break it into digits:
3
2
9Radix Sort sorts numbers digit by digit.
For each digit position, it commonly uses a stable Counting Sort.
For example:
170
045
075
090
802
024
002
066Radix Sort processes:
ones digit
↓
tens digit
↓
hundreds digitStable Counting Sort makes this possible.
So learning Counting Sort isn't just learning one algorithm.
It prepares us for:
Radix Sort
which is the next major step in this sorting-algorithm journey.
An interviewer might ask:
"Counting Sort is O(n + k). Therefore, is it always faster than Quick Sort's O(n log n)?"
The correct answer is:
No.
Because k matters.
Suppose:
n = 100
k = 1,000,000,000Then:
O(n + k)is enormous.
Meanwhile, Quick Sort can operate efficiently without allocating a billion-element count array.
So you should never look at only:
O(n)or:
O(n log n)without considering the assumptions behind the algorithm.
Is Counting Sort comparison-based?
No.
It uses the values of elements as indexes into a frequency structure.
What is the time complexity?
O(n + k)where k is the value range.
When is Counting Sort efficient?
When:
kis reasonably small relative to:
nCan Counting Sort handle negative numbers?
Yes.
Use an offset based on the minimum value:
index = num - min_valueIs Counting Sort stable?
A Counting Sort implementation can be stable.
The stable version uses:
Why is stability important?
Stability matters when sorting records by multiple keys.
For example:
Sort employees by department
then by salaryA stable sorting algorithm can preserve the ordering established by a previous sort.
Can Counting Sort sort strings directly?
Not in its basic form.
However, Counting Sort can be used to sort characters or fixed-position digits, which is one reason it is useful in algorithms such as Radix Sort.
Don't assume:
O(n + k)is automatically better than:
O(n log n)Always consider k.
Avoid:
[1, 1,000,000,000]with a billion-sized count array.
The data is sparse, and Counting Sort is a poor choice.
If negative numbers are possible, don't directly use:
count[num]Use an offset.
A frequency-based reconstruction implementation is not automatically stable.
If stability is required, use cumulative counts and an output array.
Counting Sort is a strong choice when:
✓ Data consists of integers
✓ Range of values is known or reasonably small
✓ k is not dramatically larger than n
✓ You need predictable linear-style performance
✓ Frequencies are usefulAvoid it when:
✗ Values have a massive range
✗ Data is sparse across a huge key space
✗ Elements are arbitrary objects
✗ Memory is severely constrainedSuppose you have:
10 million exam scoreswhere each score is between:
0 and 100Counting Sort is an excellent candidate.
Why?
n = 10,000,000
k = 101The range is tiny compared with the dataset.
Now imagine:
10 million transaction IDswhere IDs range from:
1 to 10 billionCounting Sort would be a poor choice.
The key lesson is:
Don't choose an algorithm only by its Big-O expression. Understand the parameters behind that expression.
The entire algorithm can be remembered as:
INPUT
↓
Find MIN and MAX
↓
Create COUNT ARRAY
↓
Count frequencies
↓
Cumulative counts (stable version)
↓
Place elements
↓
SORTED OUTPUTOr simply:
VALUES
↓
FREQUENCY
↓
PREFIX SUM
↓
POSITIONS
↓
SORTED ARRAYSo far in our sorting journey, we've moved through:
Bubble Sort
↓
Selection Sort
↓
Insertion Sort
↓
Merge Sort
↓
Quick Sort
↓
Heap Sort
↓
Counting SortWe've now crossed an important boundary.
Earlier algorithms primarily relied on comparisons.
Counting Sort shows us another paradigm:
Use additional information about the input to avoid comparisons.
This idea appears repeatedly in advanced computer science.
Before moving forward, try implementing these yourself.
Sort:
[4, 2, 2, 8, 3, 3, 1]using Counting Sort.
Sort an array containing negative numbers:
[-4, -1, -3, 2, 0, -1]Find the most frequent element using a frequency array.
Example:
[1, 3, 2, 3, 4, 3, 2]Expected:
3Given student scores between 0 and 100, sort them efficiently.
Think about why Counting Sort is a particularly good choice.
Implement stable Counting Sort for records:
[
(2, "A"),
(1, "B"),
(2, "C"),
(1, "D")
]Your result should preserve the relative order of equal keys.
Remember these points for interviews:
Counting Sort
↓
Non-comparison sorting
↓
Uses frequencies
↓
Time = O(n + k)
↓
Space = O(k)
↓
Stable version = O(n + k) space
↓
Excellent for small integer ranges
↓
Poor for huge sparse ranges
↓
Can handle negative numbers using an offset
↓
Prefix sums enable stable placement
↓
Foundation for Radix SortThe most important concept is not simply memorizing:
O(n + k)Instead, understand why k matters.
That's the difference between memorizing algorithms and actually knowing when to use them.
Pixels to Perfection Design that Impresses