{"id":"dsa-sorting-quicksort","slug":"quicksort","title":"Quicksort and partitioning","text":"**Quicksort** is another divide and conquer sort, and many languages' sort functions have been built on it. Merge sort does its real work after the recursive calls, in the merge. Quicksort does it before them:\n\n1. Pick one item, the **pivot**.\n2. **Partition** the list: move the items smaller than the pivot before it, and the rest after it. The pivot is then in its final place, the place it has in the sorted list.\n3. Sort the part before the pivot and the part after it, with a call of quicksort each. Nothing is left to combine.\n\n### Partitioning in place\n\nThe partition works inside the list itself, with no new lists. Here the pivot is the last item. The index `i` marks the end of the \"smaller\" part, so everything in `values[low:i]` is smaller than the pivot. `j` walks over the other items, and each smaller item it finds is swapped to position `i`, which grows the smaller part by one. At the end, the pivot is swapped into position `i`, between the two parts:\n\n```python\ndef partition(values, low, high):\n    pivot = values[high]\n    i = low\n    for j in range(low, high):\n        if values[j] < pivot:\n            values[i], values[j] = values[j], values[i]\n            i += 1\n    values[i], values[high] = values[high], values[i]\n    return i\n\n\nvalues = [7, 2, 9, 4, 1, 8, 5]\np = partition(values, 0, len(values) - 1)\nprint(values, \"pivot\", values[p], \"at index\", p)\n```\n\n```output\n[2, 4, 1, 5, 9, 8, 7] pivot 5 at index 3\n```\n\nNeither side is sorted yet, but 5 is exactly where it belongs, with three smaller items before it and three bigger ones after. A partition looks at each item once, O(n), and needs no extra memory.\n\n### Sorting both sides\n\n`quicksort(values, low, high)` sorts the items from `values[low]` to `values[high]`. A part with fewer than two items has nothing to sort. The pivot is left out of both calls: it's already in place, and leaving it out makes each call's part smaller, so the calls end. This version also returns how many comparisons it made, `high - low` for each partition:\n\n```python\nimport random\n\n\ndef partition(values, low, high):\n    pivot = values[high]\n    i = low\n    for j in range(low, high):\n        if values[j] < pivot:\n            values[i], values[j] = values[j], values[i]\n            i += 1\n    values[i], values[high] = values[high], values[i]\n    return i\n\n\ndef quicksort(values, low, high):\n    if low >= high:\n        return 0\n    p = partition(values, low, high)\n    comparisons = high - low\n    comparisons += quicksort(values, low, p - 1)\n    comparisons += quicksort(values, p + 1, high)\n    return comparisons\n\n\nvalues = list(range(500))\nrandom.seed(1)\nrandom.shuffle(values)\nprint(quicksort(values, 0, len(values) - 1), values[:5])\nprint(quicksort(values, 0, len(values) - 1), values[:5])\n```\n\n```output\n4751 [0, 1, 2, 3, 4]\n124750 [0, 1, 2, 3, 4]\n```\n\nOn shuffled values, the pivot usually lands somewhere near the middle, the parts roughly halve, and quicksort costs O(n log n) like merge sort. The second line sorts the same list again, now that it's sorted, and that is quicksort's **worst case**. The last item is the biggest, so each partition leaves all the other items on one side: n - 1 + n - 2 + ... comparisons, O(n²).\n\nIn Python there's a worse problem: each of those levels is one more nested call, and a sorted list of 2,000 values needs 2,000 of them, more than Python allows:\n\n```python\ndef partition(values, low, high):\n    pivot = values[high]\n    i = low\n    for j in range(low, high):\n        if values[j] < pivot:\n            values[i], values[j] = values[j], values[i]\n            i += 1\n    values[i], values[high] = values[high], values[i]\n    return i\n\n\ndef quicksort(values, low, high):\n    if low < high:\n        p = partition(values, low, high)\n        quicksort(values, low, p - 1)\n        quicksort(values, p + 1, high)\n\n\nvalues = list(range(2000))\nquicksort(values, 0, len(values) - 1)\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 20, in <module>\n    quicksort(values, 0, len(values) - 1)\n    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"main.py\", line 15, in quicksort\n    quicksort(values, low, p - 1)\n    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n  File \"main.py\", line 15, in quicksort\n    quicksort(values, low, p - 1)\n    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n  File \"main.py\", line 15, in quicksort\n    quicksort(values, low, p - 1)\n    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n  [Previous line repeated 995 more times]\n  File \"main.py\", line 14, in quicksort\n    p = partition(values, low, high)\nRecursionError: maximum recursion depth exceeded\n```\n\n### A better pivot\n\nThe cure is a pivot that is unlikely to be the biggest or the smallest item. The **middle** item is a simple choice: swap it to the end first, then partition as before. On sorted and reversed data the middle item is the best pivot there is.\n\n```python\nmiddle = (low + high) // 2\nvalues[middle], values[high] = values[high], values[middle]\n```\n\nBut the middle item has bad inputs too. In a list that rises and then falls, such as `[0, 2, 4, 6, 5, 3, 1]`, the middle item is the biggest, and round after round the pivot stays one of the biggest items left: 3,000 such values still go about 1,500 calls deep, a `RecursionError` again. Data with a shape like that is not rare, so on structured data the middle pivot's O(n²) worst case can really happen.\n\nA **random** item, `random.randint(low, high)` swapped to the end, is safer: no order of the items makes it slow every time, and seeding `random` keeps your runs repeatable. With a random pivot, quicksort is O(n log n) on average on any list of different values, and its calls stay a few dozen levels deep even for 200,000 values. The worst case is still O(n²), but now it's very unlikely. Quicksort needs no extra lists, but it isn't stable: a swap can jump an item past an equal one.\n\n### Many equal values\n\nThis two-way partition has one more weak spot, and no choice of pivot cures it: every item equal to the pivot goes to the same side. If all the items are equal, none is smaller than the pivot, so they all land on one side: the sorted case again, O(n²), and for a long list a `RecursionError`. A list with only a few different values has the same trouble. (Some two-way partitions, such as Hoare's, split the equal items between the two sides and avoid this, but they're harder to get right.) A **three-way partition** splits the items into three parts: smaller than the pivot, equal to it, and bigger. The equal part is finished, and only the other two need more sorting. It keeps three indexes: items before `lt` are smaller, items from `lt` up to `i` are equal, items after `gt` are bigger, and `i` walks through the unknown part between `i` and `gt`:\n\n```python\ndef partition3(values, pivot):\n    lt = 0\n    i = 0\n    gt = len(values) - 1\n    while i <= gt:\n        if values[i] < pivot:\n            values[lt], values[i] = values[i], values[lt]\n            lt += 1\n            i += 1\n        elif values[i] > pivot:\n            values[i], values[gt] = values[gt], values[i]\n            gt -= 1\n        else:\n            i += 1\n    return lt, gt\n\n\nvalues = [3, 1, 3, 5, 3, 2, 5, 3]\nprint(partition3(values, 3), values)\n```\n\n```output\n(2, 5) [1, 2, 3, 3, 3, 3, 5, 5]\n```\n\nThe result `(2, 5)` says that the 3s now fill indexes 2 to 5. When a bigger item is swapped to `gt`, `i` stays where it is: the item that came from the end hasn't been looked at yet. Each round either moves `i` on or moves `gt` back, so the partition is one pass, O(n).\n","task":"What does this program print? Follow the partition one item at a time. Type each line it prints.\n","kind":"predict","seed":"def partition(values, low, high):\n    pivot = values[high]\n    i = low\n    for j in range(low, high):\n        if values[j] < pivot:\n            values[i], values[j] = values[j], values[i]\n            i += 1\n    values[i], values[high] = values[high], values[i]\n    return i\n\n\nvalues = [8, 3, 6, 1, 9, 4]\nprint(partition(values, 0, 5))\nprint(values)\n","solution":"def partition(values, low, high):\n    pivot = values[high]\n    i = low\n    for j in range(low, high):\n        if values[j] < pivot:\n            values[i], values[j] = values[j], values[i]\n            i += 1\n    values[i], values[high] = values[high], values[i]\n    return i\n\n\nvalues = [8, 3, 6, 1, 9, 4]\nprint(partition(values, 0, 5))\nprint(values)\n","hints":["The pivot is the last item, 4. Only 3 and 1 are smaller, so the pivot ends up at index 2.","Follow the swaps: 3 swaps with 8 (index 0 and 1), then 1 swaps with the 8 that is now at index 1. At the end, 4 swaps with the item at index 2, which is 6."],"tests":[{"name":"Output","stdin":"","expect":"2\n[3, 1, 4, 8, 9, 6]","hidden":false}],"require":[],"forbid":[],"lines":["2","[3, 1, 4, 8, 9, 6]"],"more":[{"task":"This quicksort uses the last item as its pivot. It sorts small lists, but a sorted list of 3,000 values stops it with a `RecursionError`. Change `partition` so that it uses the middle item of `values[low]` to `values[high]` as the pivot: swap it to the end first, and the rest of the function stays as it is. Then `quicksort(values, 0, len(values) - 1)` must sort sorted and reversed lists of 3,000 values too. (A list of many equal values would still need the three-way partition, as the lesson says.) Don't use `sorted` or `sort`.\n\nFor example, this program:\n\n```python\nvalues = [6, 3, 9, 1, 8, 2, 4]\nquicksort(values, 0, len(values) - 1)\nprint(values)\n```\n\nprints:\n\n```\n[1, 2, 3, 4, 6, 8, 9]\n```\n","kind":"code","seed":"def partition(values, low, high):\n    pivot = values[high]\n    i = low\n    for j in range(low, high):\n        if values[j] < pivot:\n            values[i], values[j] = values[j], values[i]\n            i += 1\n    values[i], values[high] = values[high], values[i]\n    return i\n\n\ndef quicksort(values, low, high):\n    if low < high:\n        p = partition(values, low, high)\n        quicksort(values, low, p - 1)\n        quicksort(values, p + 1, high)\n\n\nif __name__ == \"__main__\":\n    values = [6, 3, 9, 1, 8, 2, 4]\n    quicksort(values, 0, len(values) - 1)\n    print(values)\n","solution":"def partition(values, low, high):\n    middle = (low + high) // 2\n    values[middle], values[high] = values[high], values[middle]\n    pivot = values[high]\n    i = low\n    for j in range(low, high):\n        if values[j] < pivot:\n            values[i], values[j] = values[j], values[i]\n            i += 1\n    values[i], values[high] = values[high], values[i]\n    return i\n\n\ndef quicksort(values, low, high):\n    if low < high:\n        p = partition(values, low, high)\n        quicksort(values, low, p - 1)\n        quicksort(values, p + 1, high)\n\n\nif __name__ == \"__main__\":\n    values = [6, 3, 9, 1, 8, 2, 4]\n    quicksort(values, 0, len(values) - 1)\n    print(values)\n","hints":["On a sorted list the last item is the biggest, so every partition leaves all the other items on one side: 3,000 nested calls, more than Python allows.","At the start of `partition`, find the middle index, `middle = (low + high) // 2`, and swap `values[middle]` with `values[high]`.","After that swap, the rest of `partition` stays the same: `pivot = values[high]` is now the item that was in the middle."],"tests":[{"name":"Test 1","stdin":"","call":"values = [6, 3, 9, 1, 8, 2, 4]\nquicksort(values, 0, len(values) - 1)\nprint(values)\n","expect":"[1, 2, 3, 4, 6, 8, 9]","hidden":false},{"name":"Test 2","stdin":"","call":"values = list(range(3000))\nquicksort(values, 0, len(values) - 1)\nprint(values == list(range(3000)), values[:3], values[-3:])\n","expect":"True [0, 1, 2] [2997, 2998, 2999]","hidden":true},{"name":"Test 3","stdin":"","call":"values = list(range(3000, 0, -1))\nquicksort(values, 0, len(values) - 1)\nprint(values == list(range(1, 3001)), values[:3])\n","expect":"True [1, 2, 3]","hidden":true},{"name":"Test 4","stdin":"","call":"values = [i * 37 % 101 for i in range(1000)]\nquicksort(values, 0, len(values) - 1)\nprint(values[:12], values[-3:])\n","expect":"[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] [100, 100, 100]","hidden":true},{"name":"Test 5","stdin":"","call":"values = []\nquicksort(values, 0, len(values) - 1)\nprint(values)\n","expect":"[]","hidden":true},{"name":"Test 6","stdin":"","call":"values = [2, 1]\nquicksort(values, 0, len(values) - 1)\nprint(values)\n","expect":"[1, 2]","hidden":true}],"require":[],"forbid":[{"pattern":"\\bsorted\\s*\\(|\\.sort\\s*\\(","message":"Keep quicksort; choose a better pivot instead of calling `sorted` or `sort`."},{"pattern":"setrecursionlimit","message":"Don't raise the recursion limit; a better pivot keeps the calls shallow."}]},{"task":"Support tickets have a priority of 0 (urgent), 1 (normal) or 2 (low). Write `sort_three(values)`, which sorts a list of such numbers in place with one three-way partition around 1: all the 0s first, then the 1s, then the 2s. It returns nothing. It must be fast for 200,000 tickets. Don't use `sorted` or `sort`.\n\nFor example, this program:\n\n```python\ntickets = [2, 0, 1, 2, 1, 0, 0]\nsort_three(tickets)\nprint(tickets)\n```\n\nprints:\n\n```\n[0, 0, 0, 1, 1, 2, 2]\n```\n","kind":"code","seed":"def sort_three(values):\n    pass\n\n\nif __name__ == \"__main__\":\n    tickets = [2, 0, 1, 2, 1, 0, 0]\n    sort_three(tickets)\n    print(tickets)\n","solution":"def sort_three(values):\n    lt = 0\n    i = 0\n    gt = len(values) - 1\n    while i <= gt:\n        if values[i] == 0:\n            values[lt], values[i] = values[i], values[lt]\n            lt += 1\n            i += 1\n        elif values[i] == 2:\n            values[i], values[gt] = values[gt], values[i]\n            gt -= 1\n        else:\n            i += 1\n\n\nif __name__ == \"__main__\":\n    tickets = [2, 0, 1, 2, 1, 0, 0]\n    sort_three(tickets)\n    print(tickets)\n","hints":["Use the lesson's `partition3` with the pivot 1: 0s before `lt`, 1s from `lt` up to `i`, 2s after `gt`, and the part not yet looked at from `i` to `gt`.","Look at `values[i]`: a 0 swaps to `lt` and both indexes move on; a 2 swaps to `gt` and only `gt` moves back; a 1 stays, and `i` moves on.","Loop `while i <= gt:`. The two-way partition from the lesson has trouble here: with only three different values, almost every item equals some pivot, and the calls go thousands of levels deep."],"tests":[{"name":"Test 1","stdin":"","call":"tickets = [2, 0, 1, 2, 1, 0, 0]\nsort_three(tickets)\nprint(tickets)\n","expect":"[0, 0, 0, 1, 1, 2, 2]","hidden":false},{"name":"Test 2","stdin":"","call":"tickets = []\nsort_three(tickets)\nprint(tickets)\n","expect":"[]","hidden":true},{"name":"Test 3","stdin":"","call":"tickets = [1, 1, 1]\nsort_three(tickets)\nprint(tickets)\n","expect":"[1, 1, 1]","hidden":true},{"name":"Test 4","stdin":"","call":"tickets = [2, 2, 0, 0]\nsort_three(tickets)\nprint(tickets)\n","expect":"[0, 0, 2, 2]","hidden":true},{"name":"Test 5","stdin":"","call":"tickets = [2, 1, 0]\nsort_three(tickets)\nprint(tickets)\n","expect":"[0, 1, 2]","hidden":true},{"name":"Test 6","stdin":"","call":"tickets = [i * i % 7 % 3 for i in range(3000)]\nsort_three(tickets)\nprint(tickets.count(0), tickets.count(1), tickets[:2], tickets[-2:], tickets == sorted(tickets))\n","expect":"429 1714 [0, 0] [2, 2] True","hidden":true},{"name":"Test 7","stdin":"","call":"tickets = [i * i % 7 % 3 for i in range(200_000)]\nsort_three(tickets)\nprint(tickets.count(0), tickets.count(1), tickets[:2], tickets[-2:], tickets == sorted(tickets))\n","expect":"28572 114286 [0, 0] [2, 2] True","hidden":true,"big":true}],"require":[],"forbid":[{"pattern":"\\bsorted\\s*\\(|\\.sort\\s*\\(","message":"Partition the list yourself on this step, without `sorted` or `sort`."}]}]}