{"id":"dsa-sorting-python-sort","slug":"python-sort","title":"Python's sort: keys and stability","text":"In your programs, sort with Python's own tools. `sorted(values)` returns a new sorted list and leaves `values` as it was. `values.sort()` sorts the list itself and returns `None`. Mixing them up is a classic bug:\n\n```python\nnames = [\"Mia\", \"Ann\", \"Zoe\"]\nnames = names.sort()\nprint(names[0])\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 3, in <module>\n    print(names[0])\n          ~~~~~^^^\nTypeError: 'NoneType' object is not subscriptable\n```\n\n`names.sort()` sorted the list and returned `None`, and the assignment put that `None` in `names`. Write `names.sort()` on its own line, or `names = sorted(names)`.\n\n### What Python's sort costs\n\nBoth use **Timsort**, an algorithm written in C for Python. It's a merge sort that first finds runs of items that are already in order and then merges the runs. So:\n\n- it's O(n log n), in the worst case too;\n- it's O(n) on a list that is already sorted, and fast on nearly sorted lists, like insertion sort;\n- it's **stable**: items that compare equal keep their order;\n- `sorted` makes a new list, O(n) memory.\n\nRunning in C, it's many times faster than a comparison sort written in Python, such as your own merge sort. That merge sort teaches how sorting works; in a program, use `sorted`. (Counting sort on small whole numbers, from the step before, is the exception that can beat it.)\n\n### Keys, briefly\n\n`key=` gives a function that returns what to sort each item by. Python calls it once for each item, n calls in all, not once for every comparison. A tuple as the key sorts by its first item, and by the second where the first ones are equal: `key=lambda p: (-p.points, p.name)` puts the most points first and equal points by name. For strings, remember that capital letters come before small ones (`\"Zoe\" < \"ann\"` is `True`); `key=str.lower` compares them as small letters.\n\n### Stability and sorting in several passes\n\nA minus sign turns a number's order around, but you can't put a minus sign in front of a string. Say players should be listed by team from Z to A, and by name from A to Z in each team. Stability gives a way: sort several times, the **least important** order first. Each later sort keeps the order of the items it finds equal:\n\n```python\nplayers = [(\"Ann\", \"Owls\"), (\"Bob\", \"Bees\"), (\"Cid\", \"Owls\"), (\"Ada\", \"Bees\"), (\"Eve\", \"Owls\")]\nplayers.sort(key=lambda player: player[0])\nplayers.sort(key=lambda player: player[1], reverse=True)\nfor name, team in players:\n    print(team, name)\n```\n\n```output\nOwls Ann\nOwls Cid\nOwls Eve\nBees Ada\nBees Bob\n```\n\nThe first sort puts the names in order. The second sorts by team, from Z to A, and keeps the players of each team in the order the first sort left them. `reverse=True` is stable too: equal items keep their order, they aren't reversed. That's not the same as sorting and then reversing the whole list, which reverses equal items too. Two sorts cost O(n log n) each, so still O(n log n) in all.\n\n### Sorting objects\n\nYou've sorted objects with a key that reads an attribute. A class can also define `<` itself, with the method `__lt__`: sorting compares items only with `<`, so then `sorted` works without a key. Version numbers are a good example, because as strings they sort wrongly (`\"1.10.0\"` comes before `\"1.2.5\"`):\n\n```python\nclass Version:\n    def __init__(self, text):\n        self.text = text\n        self.parts = tuple(int(part) for part in text.split(\".\"))\n\n    def __lt__(self, other):\n        return self.parts < other.parts\n\n\ntexts = [\"1.10.0\", \"1.2.5\", \"1.2.10\", \"0.9\"]\nprint(sorted(texts))\nprint([version.text for version in sorted(Version(text) for text in texts)])\n```\n\n```output\n['0.9', '1.10.0', '1.2.10', '1.2.5']\n['0.9', '1.2.5', '1.2.10', '1.10.0']\n```\n\nThe tuples `(1, 2, 10)` and `(1, 10, 0)` compare number by number, so 2 comes before 10, as it should.\n\n### Sort first, then look at neighbors\n\nSorting is often the first step of a faster algorithm. After sorting, items that are close sit side by side, so questions about pairs (repeats, the closest two, overlapping times) need only a look at each item's neighbor. Say you want the two prices that are closest to each other. Instead of comparing every pair, sort the prices and compare each one with the one just before it:\n\n```python\nprices = [42, 17, 93, 58, 25, 61]\nordered = sorted(prices)\npair = (ordered[0], ordered[1])\nfor i in range(2, len(ordered)):\n    if ordered[i] - ordered[i - 1] < pair[1] - pair[0]:\n        pair = (ordered[i - 1], ordered[i])\nprint(ordered)\nprint(pair)\n```\n\n```output\n[17, 25, 42, 58, 61, 93]\n(58, 61)\n```\n\nWhy are neighbors enough? Take two prices `a` and `c` that aren't side by side in the sorted list. The price right after `a`, call it `b`, sits between them: `a <= b <= c`, so `b - a` is at most `c - a`. The neighbors `a` and `b` are at least as close, so a pair that is further apart in the sorted order never beats the best pair of neighbors. Repeats work the same way: equal values end up side by side. The cost is O(n log n) for the sort and O(n) for the pass, instead of O(n²) for all the pairs.\n\n### Meetings: fit the most\n\nAnother interview classic starts with a sort. It's about **intervals**, pairs `(start, end)` such as meetings, where a meeting that ends at 60 doesn't clash with one that starts at 60. To fit the most meetings into one room, sort them by their end, and take each meeting that starts once the last one taken has ended:\n\n```python\ndef most_meetings(meetings):\n    chosen = []\n    for start, end in sorted(meetings, key=lambda meeting: meeting[1]):\n        if not chosen or start >= chosen[-1][1]:\n            chosen.append((start, end))\n    return chosen\n\n\nmeetings = [(60, 90), (0, 30), (25, 50), (10, 20), (100, 130), (85, 110)]\nprint(most_meetings(meetings))\n```\n\n```output\n[(10, 20), (25, 50), (60, 90), (100, 130)]\n```\n\nThat's O(n log n) for the sort and O(n) for the pass. `most_meetings` makes a **greedy** choice: it takes what looks best right now, the meeting that ends first, and never goes back on it. Greedy choices can go wrong (the module on dynamic programming shows some), but this one is safe. Take any plan with the most meetings. Its first meeting ends no earlier than the meeting that ends first of all, so swapping the two leaves room for every other meeting in the plan. So some best plan starts with the meeting that ends first, and the same argument works for each meeting after it. Sorted by start instead, (0, 30) would be taken first, and it leaves room for only 3 meetings.\n","task":"A library lists its books by year, the newest first, and the books of the same year by title from A to Z. Fill in the blank in the key of the first sort, the index of the part of a book it sorts by, so that the two sorts give that order. The program should print:\n\n```\n1987 Beloved\n1985 Contact\n1985 Ender's Game\n1969 Slaughterhouse-Five\n1969 Ubik\n1965 Dune\n```\n","kind":"fill","seed":"books = [\n    (\"Ubik\", 1969), (\"Dune\", 1965), (\"Ender's Game\", 1985),\n    (\"Slaughterhouse-Five\", 1969), (\"Beloved\", 1987), (\"Contact\", 1985),\n]\nbooks.sort(key=lambda book: book[[[0]]])\nbooks.sort(key=lambda book: book[1], reverse=True)\nfor title, year in books:\n    print(year, title)\n","solution":"books = [\n    (\"Ubik\", 1969), (\"Dune\", 1965), (\"Ender's Game\", 1985),\n    (\"Slaughterhouse-Five\", 1969), (\"Beloved\", 1987), (\"Contact\", 1985),\n]\nbooks.sort(key=lambda book: book[0])\nbooks.sort(key=lambda book: book[1], reverse=True)\nfor title, year in books:\n    print(year, title)\n","hints":["The second sort puts the years in order. The first sort must set up the order that books of the same year keep: which one is that?","Sort by title first, the less important order. The title is the first part of each tuple, `book[0]`."],"tests":[{"name":"Output","stdin":"","expect":"1987 Beloved\n1985 Contact\n1985 Ender's Game\n1969 Slaughterhouse-Five\n1969 Ubik\n1965 Dune","hidden":false}],"require":[],"forbid":[],"more":[{"task":"What does this program print? Type each line it prints.\n","kind":"predict","seed":"scores = [(\"Ann\", 3), (\"Bob\", 5), (\"Cid\", 3), (\"Dee\", 5)]\nfor name, score in sorted(scores, key=lambda s: s[1], reverse=True):\n    print(name, score)\nbackwards = sorted(scores, key=lambda s: s[1])[::-1]\nprint(backwards[0][0], backwards[-1][0])\n","solution":"scores = [(\"Ann\", 3), (\"Bob\", 5), (\"Cid\", 3), (\"Dee\", 5)]\nfor name, score in sorted(scores, key=lambda s: s[1], reverse=True):\n    print(name, score)\nbackwards = sorted(scores, key=lambda s: s[1])[::-1]\nprint(backwards[0][0], backwards[-1][0])\n","hints":["`reverse=True` puts the 5s first, and it's still stable: Bob and Dee keep the order they have in the list, and so do Ann and Cid.","The last lines sort from low to high, `Ann, Cid, Bob, Dee`, and then reverse the whole list, which turns equal scores around too."],"tests":[{"name":"Output","stdin":"","expect":"Bob 5\nDee 5\nAnn 3\nCid 3\nDee Ann","hidden":false}],"require":[],"forbid":[],"lines":["Bob 5","Dee 5","Ann 3","Cid 3","Dee Ann"]},{"task":"Write `busy_times(meetings)`. Each meeting is a tuple `(start, end)` of minutes. Return the times when the room is busy: a list of `(start, end)` tuples, ordered by start, where meetings that overlap or touch are **merged** into one. A meeting that ends at 75 and one that starts at 75 make one busy stretch, as there's no free minute between them. It must be fast for 60,000 meetings: sort them first, then make one pass.\n\nFor example, `busy_times([(45, 75), (0, 20), (15, 40), (5, 10), (75, 80)])` returns:\n\n```\n[(0, 40), (45, 80)]\n```\n","kind":"code","seed":"def busy_times(meetings):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(busy_times([(45, 75), (0, 20), (15, 40), (5, 10), (75, 80)]))\n","solution":"def busy_times(meetings):\n    merged = []\n    for start, end in sorted(meetings):\n        if merged and start <= merged[-1][1]:\n            merged[-1] = (merged[-1][0], max(merged[-1][1], end))\n        else:\n            merged.append((start, end))\n    return merged\n\n\nif __name__ == \"__main__\":\n    print(busy_times([(45, 75), (0, 20), (15, 40), (5, 10), (75, 80)]))\n","hints":["Sort by start this time: tuples sort by their first item, so `sorted(meetings)` does it. Then a meeting can only join the last busy stretch you've found so far, `merged[-1]`.","If the meeting starts no later than that stretch ends, `start <= merged[-1][1]`, it joins the stretch: replace `merged[-1]` with a tuple that has the same start and the bigger of the two ends. Otherwise it starts a new stretch, with `append`.","Take the bigger end with `max`: in the example, (5, 10) lies inside (0, 20) and must not cut its end short. And check that `merged` isn't empty before you look at `merged[-1]`."],"tests":[{"name":"busy_times([(45, 75), (0, 20), (15, 40), (5, 10), (75, 80)])","stdin":"","call":"busy_times([(45, 75), (0, 20), (15, 40), (5, 10), (75, 80)])\n","expect":"[(0, 40), (45, 80)]","hidden":false,"returns":true},{"name":"busy_times([(40, 45), (0, 10), (20, 30), (5, 6)])","stdin":"","call":"busy_times([(40, 45), (0, 10), (20, 30), (5, 6)])\n","expect":"[(0, 10), (20, 30), (40, 45)]","hidden":true,"returns":true},{"name":"busy_times([])","stdin":"","call":"busy_times([])\n","expect":"[]","hidden":true,"returns":true},{"name":"busy_times([(10, 20)])","stdin":"","call":"busy_times([(10, 20)])\n","expect":"[(10, 20)]","hidden":true,"returns":true},{"name":"busy_times([(0, 100), (10, 20), (30, 40)])","stdin":"","call":"busy_times([(0, 100), (10, 20), (30, 40)])\n","expect":"[(0, 100)]","hidden":true,"returns":true},{"name":"busy_times([(5, 10), (0, 3), (20, 30), (10, 15), (3, 4)])","stdin":"","call":"busy_times([(5, 10), (0, 3), (20, 30), (10, 15), (3, 4)])\n","expect":"[(0, 4), (5, 15), (20, 30)]","hidden":true,"returns":true},{"name":"busy_times([(50, 60), (10, 20), (55, 70), (15, 30), (70, 80)])","stdin":"","call":"busy_times([(50, 60), (10, 20), (55, 70), (15, 30), (70, 80)])\n","expect":"[(10, 30), (50, 80)]","hidden":true,"returns":true},{"name":"Test 8","stdin":"","call":"times = busy_times([(i * 7919 % 60_000 * 10, i * 7919 % 60_000 * 10 + 5) for i in range(60_000)])\nprint(len(times), times[:2], times[-1])\n","expect":"60000 [(0, 5), (10, 15)] (599990, 599995)","hidden":true,"big":true},{"name":"busy_times([(i * 7919 % 60_000 * 10, i * 7919 % 60_000 * 10 + 10) for i in range(60_000)])","stdin":"","call":"busy_times([(i * 7919 % 60_000 * 10, i * 7919 % 60_000 * 10 + 10) for i in range(60_000)])\n","expect":"[(0, 600000)]","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[]}]}