{"id":"dsa-sorting-counting","slug":"counting-sort","title":"Counting sort","text":"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.\n\n### Counting instead of comparing\n\n**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:\n\n```python\nrolls = [3, 6, 1, 3, 5, 6, 3, 2]\ncounts = [0] * 7\nfor roll in rolls:\n    counts[roll] += 1\nprint(counts)\n\nresult = []\nfor value in range(7):\n    result += [value] * counts[value]\nprint(result)\n```\n\n```output\n[0, 1, 1, 3, 0, 1, 2]\n[1, 2, 3, 3, 3, 5, 6, 6]\n```\n\nA 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]`.\n\n### What it costs\n\nCounting 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.\n\nThat 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.\n\nIt'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.\n\nValues 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).\n\nA 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.\n\n### Sorting records by a small number\n\nOften 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:\n\n```python\ntasks = [(\"email\", 2), (\"deploy\", 0), (\"lunch\", 3), (\"backup\", 0), (\"review\", 2)]\nbuckets = [[] for _ in range(4)]\nfor task in tasks:\n    buckets[task[1]].append(task)\nprint(buckets)\n\nresult = []\nfor bucket in buckets:\n    result += bucket\nprint(result)\n```\n\n```output\n[[('deploy', 0), ('backup', 0)], [], [('email', 2), ('review', 2)], [('lunch', 3)]]\n[('deploy', 0), ('backup', 0), ('email', 2), ('review', 2), ('lunch', 3)]\n```\n\n`[[] 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.\n","task":"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:\n\n```\n[-7, -3, -3, 0, 2, 5, 9]\n```\n","kind":"fill","seed":"temperatures = [2, -3, 9, 0, -7, 5, -3]\ncounts = [0] * 21\nfor t in temperatures:\n    counts[[[t + 10‖10 + t]]] += 1\n\nresult = []\nfor index in range(21):\n    temperature = [[index - 10‖-10 + index]]\n    result += [temperature] * counts[index]\nprint(result)\n","solution":"temperatures = [2, -3, 9, 0, -7, 5, -3]\ncounts = [0] * 21\nfor t in temperatures:\n    counts[t + 10] += 1\n\nresult = []\nfor index in range(21):\n    temperature = index - 10\n    result += [temperature] * counts[index]\nprint(result)\n","hints":["-10 has the counter 0, -9 the counter 1, and so on up to 10, which has the counter 20. What do you add to a temperature to get its counter?","The counter of `t` is `t + 10`. Going back, the counter `index` stands for the temperature `index - 10`."],"tests":[{"name":"Output","stdin":"","expect":"[-7, -3, -3, 0, 2, 5, 9]","hidden":false}],"require":[],"forbid":[],"more":[{"task":"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`.\n\nFor example, `counting_sort([6, 2, 0, 6, 3], 8)` returns:\n\n```\n[0, 2, 3, 6, 6]\n```\n","kind":"code","seed":"def counting_sort(values, max_value):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(counting_sort([6, 2, 0, 6, 3], 8))\n","solution":"def counting_sort(values, max_value):\n    counts = [0] * (max_value + 1)\n    for value in values:\n        counts[value] += 1\n    result = []\n    for value in range(max_value + 1):\n        result += [value] * counts[value]\n    return result\n\n\nif __name__ == \"__main__\":\n    print(counting_sort([6, 2, 0, 6, 3], 8))\n","hints":["Make a list of `max_value + 1` counters, all 0, and add one to `counts[value]` for each value.","Then go through the counters in order and add each value as many times as it was counted: `result += [value] * counts[value]`.","`values.count(value)` for each possible value gives the right answer too, but each call goes through all the values: O(n·k), billions of steps here."],"tests":[{"name":"counting_sort([6, 2, 0, 6, 3], 8)","stdin":"","call":"counting_sort([6, 2, 0, 6, 3], 8)\n","expect":"[0, 2, 3, 6, 6]","hidden":false,"returns":true},{"name":"counting_sort([], 3)","stdin":"","call":"counting_sort([], 3)\n","expect":"[]","hidden":true,"returns":true},{"name":"counting_sort([4, 4, 4], 4)","stdin":"","call":"counting_sort([4, 4, 4], 4)\n","expect":"[4, 4, 4]","hidden":true,"returns":true},{"name":"counting_sort([7, 0, 7, 3, 0, 1], 7)","stdin":"","call":"counting_sort([7, 0, 7, 3, 0, 1], 7)\n","expect":"[0, 0, 1, 3, 7, 7]","hidden":true,"returns":true},{"name":"counting_sort([5], 10)","stdin":"","call":"counting_sort([5], 10)\n","expect":"[5]","hidden":true,"returns":true},{"name":"Test 6","stdin":"","call":"result = counting_sort([i * i % 100_003 for i in range(200_000)], 100_002)\nprint(len(result), result[:6], result[-3:])\n","expect":"200000 [0, 0, 1, 1, 1, 4] [100001, 100001, 100001]","hidden":true,"big":true}],"require":[],"forbid":[{"pattern":"\\bsorted\\s*\\(|\\.sort\\s*\\(","message":"Count the values yourself on this step, without `sorted` or `sort`."}]},{"task":"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`.\n\nFor example, this program:\n\n```python\nreviews = [(\"Ana\", 4), (\"Ben\", 5), (\"Cal\", 4), (\"Dee\", 1), (\"Eve\", 5)]\nfor name, stars in sort_by_stars(reviews):\n    print(name, stars)\n```\n\nprints:\n\n```\nBen 5\nEve 5\nAna 4\nCal 4\nDee 1\n```\n","kind":"code","seed":"def sort_by_stars(reviews):\n    pass\n\n\nif __name__ == \"__main__\":\n    reviews = [(\"Ana\", 4), (\"Ben\", 5), (\"Cal\", 4), (\"Dee\", 1), (\"Eve\", 5)]\n    for name, stars in sort_by_stars(reviews):\n        print(name, stars)\n","solution":"def sort_by_stars(reviews):\n    buckets = [[] for _ in range(6)]\n    for review in reviews:\n        buckets[review[1]].append(review)\n    result = []\n    for stars in range(5, 0, -1):\n        result += buckets[stars]\n    return result\n\n\nif __name__ == \"__main__\":\n    reviews = [(\"Ana\", 4), (\"Ben\", 5), (\"Cal\", 4), (\"Dee\", 1), (\"Eve\", 5)]\n    for name, stars in sort_by_stars(reviews):\n        print(name, stars)\n","hints":["Make 6 empty buckets, `[[] for _ in range(6)]`, so that `buckets[stars]` exists for every number of stars from 1 to 5 (bucket 0 stays empty).","Append each review to the bucket of its stars, then add the buckets to the result from 5 down to 1: `for stars in range(5, 0, -1):`.","Don't make the buckets with `[[]] * 6`: that's one list six times over, so every review would land in all the buckets at once."],"tests":[{"name":"Test 1","stdin":"","call":"reviews = [(\"Ana\", 4), (\"Ben\", 5), (\"Cal\", 4), (\"Dee\", 1), (\"Eve\", 5)]\nfor name, stars in sort_by_stars(reviews):\n    print(name, stars)\n","expect":"Ben 5\nEve 5\nAna 4\nCal 4\nDee 1","hidden":false},{"name":"sort_by_stars([])","stdin":"","call":"sort_by_stars([])\n","expect":"[]","hidden":true,"returns":true},{"name":"sort_by_stars([(\"a\", 3), (\"b\", 3), (\"c\", 3)])","stdin":"","call":"sort_by_stars([(\"a\", 3), (\"b\", 3), (\"c\", 3)])\n","expect":"[('a', 3), ('b', 3), ('c', 3)]","hidden":true,"returns":true},{"name":"sort_by_stars([(\"x\", 1), (\"y\", 2), (\"z\", 3), (\"w\", 4), (\"v\", 5)])","stdin":"","call":"sort_by_stars([(\"x\", 1), (\"y\", 2), (\"z\", 3), (\"w\", 4), (\"v\", 5)])\n","expect":"[('v', 5), ('w', 4), ('z', 3), ('y', 2), ('x', 1)]","hidden":true,"returns":true},{"name":"sort_by_stars([(\"Mo\", 2), (\"Li\", 5), (\"Jo\", 2), (\"Ty\", 1), (\"Al\", 5), (\"Sy\", 2), (\"Bo\", 1)])","stdin":"","call":"sort_by_stars([(\"Mo\", 2), (\"Li\", 5), (\"Jo\", 2), (\"Ty\", 1), (\"Al\", 5), (\"Sy\", 2), (\"Bo\", 1)])\n","expect":"[('Li', 5), ('Al', 5), ('Mo', 2), ('Jo', 2), ('Sy', 2), ('Ty', 1), ('Bo', 1)]","hidden":true,"returns":true}],"require":[],"forbid":[{"pattern":"\\bsorted\\s*\\(|\\.sort\\s*\\(","message":"Use buckets on this step, without `sorted` or `sort`."}]}]}