Python Arena
Step 4 of 6

Counting sort

Every sort so far compares two items at a time, and a sort that only compares can't do better than O(n log n) in general: there are too many possible orders to tell apart with fewer comparisons. But when the values are small whole numbers, you don't need to compare them at all. You can count them.

Counting instead of comparing

Counting sort sorts whole numbers from 0 up to some largest value, k. It makes a list of k + 1 counters, one for each possible value, goes through the values once and adds one to each value's counter, and then writes each value out as many times as it was counted:

rolls = [3, 6, 1, 3, 5, 6, 3, 2]
counts = [0] * 7
for roll in rolls:
    counts[roll] += 1
print(counts)

result = []
for value in range(7):
    result += [value] * counts[value]
print(result)
Output
[0, 1, 1, 3, 0, 1, 2]
[1, 2, 3, 3, 3, 5, 6, 6]

A die shows 1 to 6, so 7 counters cover the values 0 to 6 (counter 0 stays at 0). The value is the index of its own counter, so counting one value is O(1). [value] * counts[value] is a list of the value repeated: [3] * 3 is [3, 3, 3].

What it costs

Counting goes through the n values once, O(n). Writing them out goes through the k + 1 counters and adds the n values, O(n + k). So counting sort is O(n + k) in time, and needs O(k) memory for the counters, besides the result.

That beats the O(n log n) of comparison sorts when the range k is small next to the number of values n: a million exam scores from 0 to 100, ages, star ratings, dice. Python's own sorted runs in C and is hard to beat with Python code, yet on a million scores from 0 to 100, counting sort in plain Python is even faster.

It's a poor fit when the range is huge. Ten values up to a billion would need a billion counters: far more memory and time than sorting ten values. And it works only for whole numbers, or things you can number.

Values that don't start at 0 are shifted. For temperatures from -20 to 40, counter 0 is for -20, so the temperature t has the counter t + 20, and 61 counters cover the range. min(values) and max(values) find the range in O(n).

A tempting slow way is to count each possible value with values.count(value). Each call goes through the whole list, so that's O(n·k): for 200,000 values from 0 to 100,000, 20 billion comparisons.

Sorting records by a small number

Often the things you sort are records, like tuples or objects, and the small number is one part of each: tasks by priority, reviews by stars. The records have to stay whole, so instead of counters, use buckets: one list for each value. Put each record in the bucket of its number, then join the buckets in order:

tasks = [("email", 2), ("deploy", 0), ("lunch", 3), ("backup", 0), ("review", 2)]
buckets = [[] for _ in range(4)]
for task in tasks:
    buckets[task[1]].append(task)
print(buckets)

result = []
for bucket in buckets:
    result += bucket
print(result)
Output
[[('deploy', 0), ('backup', 0)], [], [('email', 2), ('review', 2)], [('lunch', 3)]]
[('deploy', 0), ('backup', 0), ('email', 2), ('review', 2), ('lunch', 3)]

[[] for _ in range(4)] makes four separate empty lists (_ is the usual name for a loop variable you don't use). The records in a bucket stay in the order they came in, so this sort is stable: deploy before backup, email before review. It's O(n + k) again.

Your task

Challenge 1 of 3

The program sorts a week of night temperatures, whole degrees from -10 to 10, with counting sort. Counter 0 is for -10, so the temperature t has the counter t + 10. Fill in the two blanks: the counter to add one to, and the temperature that counter index stands for. The program should print:

[-7, -3, -3, 0, 2, 5, 9]

Challenge 2

Write counting_sort(values, max_value), which returns a new list with values sorted. The values are whole numbers from 0 to max_value. Use counting sort, so that it's fast for 200,000 values up to 100,000. Don't use sorted or sort.

For example, counting_sort([6, 2, 0, 6, 3], 8) returns:

[0, 2, 3, 6, 6]

Challenge 3

Write sort_by_stars(reviews), where reviews is a list of (name, stars) tuples and stars is a whole number from 1 to 5. Return a new list with the 5-star reviews first, then the 4-star ones, and so on down to 1 star, with reviews of the same stars in the order they have in the list. Use buckets, as in the lesson, not sorted or sort.

For example, this program:

reviews = [("Ana", 4), ("Ben", 5), ("Cal", 4), ("Dee", 1), ("Eve", 5)]
for name, stars in sort_by_stars(reviews):
    print(name, stars)

prints:

Ben 5
Eve 5
Ana 4
Cal 4
Dee 1