DSA Quick Sort

Quick sort selects a pivot element, partitions the rest of the list around it, and recursively sorts the two resulting partitions.

The Partition Step

Partitioning is the heart of quick sort. Given a pivot value, the partition step rearranges the array so that every element smaller than the pivot ends up to its left, and every element greater ends up to its right. The pivot then sits at its final sorted position, and the two partitions on either side can be sorted independently and recursively.

  • Choose a pivot element from the sub-array.
  • Rearrange elements so smaller values move left of the pivot and larger values move right.
  • Place the pivot at the boundary between the two groups.
  • Recursively apply the same process to the left and right partitions.
  • Stop recursing once a partition has zero or one element.

Example

def partition(arr, low, high):
    pivot = arr[high]
    i = low - 1
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

def quick_sort(arr, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low < high:
        pivot_index = partition(arr, low, high)
        quick_sort(arr, low, pivot_index - 1)
        quick_sort(arr, pivot_index + 1, high)
    return arr

print(quick_sort([10, 7, 8, 9, 1, 5]))

Choosing a Pivot

The choice of pivot determines how balanced the partitions are. Always picking the first or last element works fine on random data but performs badly on already-sorted or reverse-sorted input, where every partition splits off just one element at a time. Choosing a random element, or the median of the first, middle, and last elements, avoids these adversarial cases in practice.

Example

import random

def partition_random(arr, low, high):
    random_index = random.randint(low, high)
    arr[random_index], arr[high] = arr[high], arr[random_index]
    return partition(arr, low, high)

def quick_sort_random(arr, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low < high:
        pivot_index = partition_random(arr, low, high)
        quick_sort_random(arr, low, pivot_index - 1)
        quick_sort_random(arr, pivot_index + 1, high)
    return arr

print(quick_sort_random([33, 10, 55, 71, 29, 3]))
Note: If the pivot is consistently the smallest or largest remaining value — for example, always picking the last element on an already-sorted array — quick sort degrades to O(n^2) because every partition only removes one element.

Complexity Analysis

CaseTime ComplexitySpace Complexity
Best (balanced partitions)O(n log n)O(log n)
AverageO(n log n)O(log n)
Worst (bad pivot choice)O(n^2)O(n)

Example

def quick_sort_functional(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort_functional(left) + middle + quick_sort_functional(right)

print(quick_sort_functional([8, 4, 7, 3, 10, 2]))
Note: Randomizing the pivot choice or using median-of-three selection makes the O(n^2) worst case extremely unlikely to occur in practice, which is why quick sort often outperforms merge sort in real-world use despite sharing the same average complexity.

Exercise: DSA Quick Sort

What is the core strategy quick sort uses to sort a list?