DSA course menu
Quick Sort in Python: Partitioning, Pivots and Quickselect
Learn quick sort: choosing a pivot, the Lomuto partition scheme, in-place recursive sorting, why it is O(n log n) on average but O(n²) in the worst case, random pivots, and quickselect for the kth smallest item.
On this page
Quick sort is another divide and conquer sort, but it does its work before recursing:
- Pick a pivot item.
- Partition: rearrange the list so everything smaller than the pivot is on its left and everything larger is on its right. The pivot is now in its final position.
- Recursively sort the left part and the right part.
There is no merge step: once both sides are sorted, the whole list is.
A simple (not in-place) version#
This version shows the idea clearly, at the cost of building new lists:
def quick_sort_simple(items):
if len(items) <= 1:
return items
pivot = items[len(items) // 2]
smaller = [x for x in items if x < pivot]
equal = [x for x in items if x == pivot]
larger = [x for x in items if x > pivot]
return quick_sort_simple(smaller) + equal + quick_sort_simple(larger)
print(quick_sort_simple([3, 6, 8, 10, 1, 2, 1]))[1, 1, 2, 3, 6, 8, 10]In-place partitioning (Lomuto scheme)#
The real algorithm partitions within the list itself, using O(1) extra memory per call. Take the last item as the pivot; walk the list keeping a boundary i: everything before i is smaller than the pivot.
def partition(items, low, high):
pivot = items[high]
i = low # next slot for a "small" item
for j in range(low, high):
if items[j] < pivot:
items[i], items[j] = items[j], items[i]
i += 1
items[i], items[high] = items[high], items[i] # pivot into place
return i
data = [10, 80, 30, 90, 40, 50, 70]
p = partition(data, 0, len(data) - 1)
print(data, "pivot index", p)[10, 30, 40, 50, 70, 90, 80] pivot index 4The pivot 70 is now at index 4; everything to its left is smaller and everything to its right is larger.
The full in-place sort#
import random
def partition(items, low, high):
pivot = items[high]
i = low
for j in range(low, high):
if items[j] < pivot:
items[i], items[j] = items[j], items[i]
i += 1
items[i], items[high] = items[high], items[i]
return i
def quick_sort(items, low=0, high=None):
if high is None:
high = len(items) - 1
if low < high:
p = partition(items, low, high)
quick_sort(items, low, p - 1)
quick_sort(items, p + 1, high)
data = [10, 80, 30, 90, 40, 50, 70]
quick_sort(data)
print(data)
random.seed(3)
for _ in range(100):
xs = [random.randint(0, 50) for _ in range(random.randint(0, 30))]
expected = sorted(xs)
quick_sort(xs)
assert xs == expected
print("100 random lists sorted correctly")[10, 30, 40, 50, 70, 80, 90]
100 random lists sorted correctlyGood and bad pivots#
If each pivot splits the list roughly in half, there are log n levels of O(n) partitioning: O(n log n). If the pivot is always the smallest or largest item, each partition only removes one item: n levels, O(n²).
Choosing the last item as the pivot hits the worst case on input that is already sorted. Counting comparisons shows it:
import random, sys
sys.setrecursionlimit(5000)
comparisons = 0
def partition(items, low, high, randomize):
global comparisons
if randomize:
r = random.randint(low, high)
items[r], items[high] = items[high], items[r]
pivot, i = items[high], low
for j in range(low, high):
comparisons += 1
if items[j] < pivot:
items[i], items[j] = items[j], items[i]
i += 1
items[i], items[high] = items[high], items[i]
return i
def quick_sort(items, low, high, randomize):
if low < high:
p = partition(items, low, high, randomize)
quick_sort(items, low, p - 1, randomize)
quick_sort(items, p + 1, high, randomize)
random.seed(11)
n = 2000
for label, randomize in [("last-item pivot", False), ("random pivot", True)]:
comparisons = 0
data = list(range(n)) # already sorted input
quick_sort(data, 0, n - 1, randomize)
print(f"{label:>16}: {comparisons:>8} comparisons on sorted input") last-item pivot: 1999000 comparisons on sorted input
random pivot: 24065 comparisons on sorted inputA random pivot makes the bad case extremely unlikely for any input, giving O(n log n) on average. (The setrecursionlimit call is needed only because the bad pivot recurses n levels deep.)
Quick sort is usually faster than merge sort in practice because it sorts in place and works on neighbouring memory; C's qsort and C++'s std::sort use variants of it.
Quickselect: the kth smallest item#
To find the kth smallest item you do not need to sort everything. Partition once; the pivot lands at its final index p. If p == k, done. Otherwise recurse into only one side:
import random
def quickselect(items, k):
items = items[:]
low, high = 0, len(items) - 1
while True:
r = random.randint(low, high)
items[r], items[high] = items[high], items[r]
pivot, i = items[high], low
for j in range(low, high):
if items[j] < pivot:
items[i], items[j] = items[j], items[i]
i += 1
items[i], items[high] = items[high], items[i]
if i == k:
return items[i]
if i < k:
low = i + 1
else:
high = i - 1
data = [7, 10, 4, 3, 20, 15]
print([quickselect(data, k) for k in range(len(data))])
print(sorted(data))[3, 4, 7, 10, 15, 20]
[3, 4, 7, 10, 15, 20]Each round discards one side, so the average work is n + n/2 + n/4 + … ≈ 2n: O(n) on average.
Common mistakes#
Practice#
Practice exercise
Many equal values make Lomuto partitioning slow. Write sort_colors(nums) that sorts a list containing only 0s, 1s and 2s in one pass and in place, using a three-way partition (the "Dutch national flag" problem).
Show hint
Keep three pointers: low (next place for a 0), mid (current item) and high (next place for a 2, from the end). A 0 swaps to low, a 2 swaps to high, and a 1 just moves mid forward. Do not advance mid after swapping with high: the item that came back has not been checked.
Show solution
def sort_colors(nums):
low, mid, high = 0, 0, len(nums) - 1
while mid <= high:
if nums[mid] == 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
elif nums[mid] == 2:
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
else:
mid += 1
return nums
print(sort_colors([2, 0, 2, 1, 1, 0]))
print(sort_colors([2, 1, 0]))[0, 0, 1, 1, 2, 2]
[0, 1, 2]Three-way partitioning is also what keeps quick sort fast on lists with many duplicates.
Key takeaway#
Next: sorting without comparisons, in Counting and Radix Sort.
Finished this lesson?