|Worst case time|
|Best case time|
|Average case time|
- Linear time. Counting sort runs in time, making it asymptotically faster than comparison-based sorting algorithms like quicksort or merge sort.
- Restricted inputs. Counting sort only works when the range of potential items in the input is known ahead of time.
- Space cost. If the range of potential values is big, then counting sort requires a lot of space (perhaps more than ).
The High-Level Idea
Counting sort works by iterating through the input, counting the number of times each item occurs, and using those counts to compute an item's index in the final, sorted array.
Counting How Many Times Each Item Occurs
Say we have this array:
And say we know all the numbers in our array will be whole numbers between 0 and 10 (inclusive).
The idea is to count how many 0's we see, how many 1's we see, and so on. Since there are 11 possible values, we'll use an array with 11 counters, all initialized to 0.
Couldn't we use a hash map instead? We could, but since we're working with items that can easily be mapped to array indices, using an array is a bit more lightweight. Remember: hash maps are built on top of arrays.
We'll iterate through the input once. The first item is a 4, so we'll add one to counts. The next item is an 8, so we'll add one to counts.
And so on. When we reach the end, we'll have the total counts for each number:
Building the Sorted Output
Now that we know how many times each item appears, we can fill in our sorted array. Looking at counts, we don't have any 0's or 1's, but we've got two 2's. So, those go at the start of our sorted array.
No 3's, but there are two 4's that come next.
After that, we have one 6,
and three 9's
And, with that, we're done!
Handling Non-Numeric Items
What if the items in our input aren't simple numbers that we can extract from our counts array?
As an example, what if we had an array of dessert objects, and we wanted to sort them by price?
We'll use these icons to represent the objects:
If we went through and counted up all the prices, we'd end up with the same counts array.
But when we go to add the 2's into our sorted array, we need to get the actual dessert objects.
There are a few ways we could do this:
- Iterate through the input to find both of the desserts that cost $2. That takes time for each cost, so time overall.
- Build a hash map mapping prices to desserts. (Maybe this could even replace counts.) We could do this without adding any asymptotic time cost, but this would basically be making an extra copy of the input, which would be extra space.
We can do this in time without any extra copies of the input.
Building a Next Index Array
Using our counts array, we can pre-compute where each item from the input should go. Then we'll just iterate through the input, using the pre-computed indices to place each item in the right spot.
We'll use our counts array to build up another array, nextIndex, that will track where the next occurrence of a price goes in our sorted output. For instance, nextIndex would hold the index for the next item with a price of $4.
Hold up. Doesn't this add space to our algorithm?
Hang tight ... we'll see that with some cleverness we won't need a separate array for nextIndex after all.
We can initialize nextIndex from our counts array.
The lowest price is $2, so the $2 items will start at index 0. (Nothing costs $0 or $1, so we'll just set those to 0.)
Items that cost $3 go after all the items that cost $2. Two items in the array cost $2, so they'll take up indices 0 and 1. That means the first $3 item would go at index 2.
Notice how nextIndex = nextIndex + counts. That makes sense, because we're taking the starting point for the $2 items and moving forward to make room for each of them. In general:
We can keep iterating through counts using this formula to fill in nextIndex.
As we'll see, we actually don't need counts anymore after we've built nextIndex.
So instead of creating a separate array for nextIndex, we can just modify counts in-place in one pass to get our nextIndex array. It's slightly trickier, but it can be done :)
Building Our Sorted array With nextIndex
First up, we've got
It has a price of $4. Since nextIndex is 2, we know it goes at index 2.
And, since we've placed something at index 2, we now know that the next item that costs $4 goes after it, at index 3. So we'll increment nextIndex.
Moving on to the next item, we've got
It costs $8. Since nextIndex is 5, it goes at index 5:
And we increment nextIndex:
Next comes . It also costs $4, just like .
Good thing we incremented nextIndex when we added to our sorted output! Else we'd just write our over at index 2 in our sorted output.
Speaking of, let's make sure we update nextIndex again:
Next up :
And we update nextIndex:
And so on, until we're done!
Here's how we'd code it up:
What if the values could be negative? Or the smallest value was 50?
Our implementation assumes that all of the items are between 0 and some maximum. But it's pretty simple to extend the algorithm to handle any sort of range of integers. Give it a try. :)
Counting sort takes time and space, where n is the number of items we're sorting and k is the number of possible values.
We iterate through the input items twice—once to populate counts and once to fill in the output array. Both iterations are time. Additionally, we iterate through counts once to fill in nextIndex, which is time.
The algorithm allocates three additional arrays: one for counts, one for nextIndex, and one for the output. The first two are space and the final one is space.
You can actually combine counts and nextIndex into one array. No asymptotic changes, but it does save space.
In many cases cases, k is (i.e.: the number of items to be sorted is not asymptotically different than the number of values those items can take on. Because of this, counting sort is often said to be time and space.