The input [1, 3, 7, 8, 1, 1, 3] has three (1)'s, two (3)'s, and a (7) and (8). Encoding that in a list of counts, we have [0, 3, 2, 0, 0, 0, 1, 1, 0, 0].

Counting Sort Algorithm

Quick reference

Complexity
Worst case time
Best case time
Average case time
Space

Strengths:

  • Linear time. Counting sort runs in time, making it asymptotically faster than comparison-based sorting algorithms like quicksort or merge sort.

Weaknesses:

  • 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 list.

Counting How Many Times Each Item Occurs

Say we have this list:

Unsorted input: [4, 8, 4, 2, 9, 9, 6, 2, 9].

And say we know all the numbers in our list 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 a list with 11 counters, all initialized to 0.

Couldn't we use a dictionary instead? We could, but since we're working with items that can easily be mapped to list indices, using a list is a bit more lightweight. Remember: dictionaries are built on top of lists.

List of counters: [0 zeros, 0 ones, 0 twos, 0 threes, 0 fours, 0 fives, 0 sixes, 0 sevens, 0 eights, 0 nines, 0 tens].

We'll iterate through the input once. The first item is a 4, so we'll add one to counts[4]. The next item is an 8, so we'll add one to counts[8].

The first two elements in the input [4, 8, 4, 2, ...] are 4 and 8. To count them, we increment the value at indices 4 and 8 in our counts list, which becomes [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0].

And so on. When we reach the end, we'll have the total counts for each number:

Once we count all the values in [4, 8, 4, 2, 9, 9, 6, 2, 9], the counts list is [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].

Building the Sorted Output

Now that we know how many times each item appears, we can fill in our sorted list. 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 list.

Our sorted output is [2, 2, _, _, _, _, _, _, _], because we counted two (2)'s in the input.

No 3's, but there are two 4's that come next.

Accounting for the two (4)'s, the sorted output becomes [2, 2, 4, 4, _, _, _, _, _].

After that, we have one 6,

Accounting for the single (6), the sorted output becomes [2, 2, 4, 4, 6, _, _, _, _].

one 8,

Accounting for the single (8), the sorted output becomes [2, 2, 4, 4, 6, 8, _, _, _].

and three 9's

Finally, our three (9)'s go at the end: [2, 2, 4, 4, 6, 8, 9, 9, 9].

And, with that, we're done!

Final sorted output: [2, 2, 4, 4, 6, 8, 9, 9, 9].

Handling Non-Numeric Items

What if the items in our input aren't simple numbers that we can extract from our counts list?

As an example, what if we had a list of dessert objects, and we wanted to sort them by price?

[(type: macadamia nut cookie, price: 4), (type: double chocolate cake, price: 8), (type: chocolate chip cookie, price: 4), (type: sugar cookie, price: 2), (type: creme brulee, price: 9), (type: chocolate souflee, price: 9), (type: fruit tart, price: 6), (type: brownie, price: 2), (type: eclair, price: 9)]

We'll use these icons to represent the objects:

M for "macadamia nut cookie", DCC for "double chocolate cake", CCC for "chocolate chip cookie", S for "sugar cookie", CB for "creme brulee", CS for "chocolate souflee", FT for "fruit tart", B for "brownie", and E for eclair.
Unsorted Input with prices: [M $4, DCC $8, CCC $4, S $2, CB $9, CS $9, FT $6, B $2, E $9].

If we went through and counted up all the prices, we'd end up with the same counts list.

Count list for prices: [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].

But when we go to add the 2's into our sorted list, 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 dictionary 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 List

Using our counts list, 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 list to build up another list, next_index, that will track where the next occurrence of a price goes in our sorted output. For instance, next_index[4] 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 list for next_index after all.

We can initialize next_index from our counts list.

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.)

Counts list: [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0]. Next Index list: [0, 0, 0, _, _, _, _, _, _, _].

Items that cost $3 go after all the items that cost $2. Two items in the list cost $2, so they'll take up indices 0 and 1. That means the first $3 item would go at index 2.

The third index in Next Index is calculated by adding the second index of counts (2) and the second index of Next Index (0) to get 2. The Next Index list is now [0, 0, 0, 2, _, _, _, _, _, _, _]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].

Notice how next_index[3] = next_index[2] + counts[2]. 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:

next_index[i] = next_index[i - 1] + counts[i - 1]

We can keep iterating through counts using this formula to fill in next_index.

The fourth index in Next Index is calculated by adding the third index of counts (0) and the third index of Next Index (2) to get 2. The Next Index list is now [0, 0, 0, 2, 2, _, _, _, _, _, _]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].
The fifth index in Next Index is calculated by adding the fourth index of counts (2) and the fourth index of Next Index (2) to get 4. The Next Index list is now [0, 0, 0, 2, 2, 4, _, _, _, _, _]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].
The sixth index in Next Index is calculated by adding the fifth index of counts (0) and the fifth index of Next Index (4) to get 4. The Next Index list is now [0, 0, 0, 2, 2, 4, 4, _, _, _, _]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].
The seventh index in Next Index is calculated by adding the sixth index of counts (1) and the sixth index of Next Index (4) to get 5. The Next Index list is now [0, 0, 0, 2, 2, 4, 4, 5, _, _, _]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].
The eigth index in Next Index is calculated by adding the seventh index of counts (0) and the seventh index of Next Index (5) to get 5. The Next Index list is now [0, 0, 0, 2, 2, 4, 4, 5, 5, _, _]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].
The ninth index in Next Index is calculated by adding the eigth index of counts (1) and the eigth index of Next Index (5) to get 6. The Next Index list is now [0, 0, 0, 2, 2, 4, 4, 5, 5, 6, _]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].
The tenth index in Next Index is calculated by adding the ninth index of counts (3) and the ninth index of Next Index (6) to get 9. The Next Index list is now [0, 0, 0, 2, 2, 4, 4, 5, 5, 6, 9]. Counts is still [0, 0, 2, 0, 2, 0, 1, 0, 1, 3, 0].

As we'll see, we actually don't need counts anymore after we've built next_index.

So instead of creating a separate list for next_index, we can just modify counts in-place in one pass to get our next_index list. It's slightly trickier, but it can be done :)

Building Our Sorted list With next_index

First up, we've got

It has a price of $4. Since next_index[4] is 2, we know it goes at index 2.

The first element in the unsorted input [M $4, DCC $8, CCC $4, S $2, CB $9, CS $9, FT $6, B $2, E $9] is (4). The value at the 4th index of Next Index is 2. So, the first element in the unsorted input goes at index 2 in the sorted output: [_, _, M $4, _, _, _, _, _, _].

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 next_index[4].

The value at the 4th index of Next Index is incremented from 2 to 3. Next Index is now [0, 0, 0, 2, 3, 4, 4, 5, 5, 6, 9].

Moving on to the next item, we've got

It costs $8. Since next_index[8] is 5, it goes at index 5:

The second element in the unsorted input [M $4, DCC $8, CCC $4, S $2, CB $9, CS $9, FT $6, B $2, E $9] is (8). The value at the 8th index of Next Index is 5. So, the second element in the unsorted input goes at index 5 in the sorted output: [_, _, M $4, _, _, DCC $8, _, _, _].

And we increment next_index[8]:

The value at the 8th index of Next Index is incremented from 5 to 6. Next Index is now [0, 0, 0, 2, 3, 4, 4, 5, 6, 6, 9].

Next comes . It also costs $4, just like .

The third element in the unsorted input [M $4, DCC $8, CCC $4, S $2, CB $9, CS $9, FT $6, B $2, E $9] is (4). The value at the 4th index of Next Index is 3. So, the third element in the unsorted input goes at index 3 in the sorted output: [_, _, M $4, CCC $4, _, DCC $8, _, _, _].

Good thing we incremented next_index[4] 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 next_index again:

The value at the 4th index of Next Index is incremented from 3 to 4. Next Index is now [0, 0, 0, 2, 4, 4, 4, 5, 6, 6, 9].

Next up :

The fourth element in the unsorted input [M $4, DCC $8, CCC $4, S $2, CB $9, CS $9, FT $6, B $2, E $9] is (2). The value at the 2nd index of Next Index is 0. So, the fourth element in the unsorted input goes at index 0 in the sorted output: [SC $2, _, M $4, CCC $4, _, DCC $8, _, _, _].

And we update next_index:

The value at the 2nd index of Next Index is incremented from 0 to 1. Next Index is now [0, 0, 1, 2, 4, 4, 4, 5, 6, 6, 9].

And so on, until we're done!

Implementation

Here's how we'd code it up:

def counting_sort(the_list, max_value): # Count the number of times each value appears. # counts[0] stores the number of 0's in the input # counts[4] stores the number of 4's in the input # etc. counts = [0] * (max_value + 1) for item in the_list: counts[item] += 1 # Overwrite counts to hold the next index an item with # a given value goes. So, counts[4] will now store the index # where the next 4 goes, not the number of 4's our # list has. num_items_before = 0 for i, count in enumerate(counts): counts[i] = num_items_before num_items_before += count # Output list to be filled in sorted_list = [None] * len(the_list) # Run through the input list for item in the_list: # Place the item in the sorted list sorted_list[ counts[item] ] = item # And, make sure the next item we see with the same value # goes after the one we just placed counts[item] += 1 return sorted_list

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. :)

Complexity

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 list. Both iterations are time. Additionally, we iterate through counts once to fill in next_index, which is time.

The algorithm allocates three additional lists: one for counts, one for next_index, and one for the output. The first two are space and the final one is space.

You can actually combine counts and next_index into one list. 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.

What's next?

If you're ready to start applying these concepts to some problems, check out our mock coding interview questions.

They mimic a real interview by offering hints when you're stuck or you're missing an optimization.

Try some questions now

. . .