Quiz 2
Registry Synced

07. Quick Sort — The Fastest Practical Sort

1892 words
9 min read

Reading compass

Now · 1. The Key Insight: Partition Without Merging

07. Quick Sort — The Fastest Practical Sort

What problem does this solve? Merge sort is fast but uses O(n) extra memory. Quick sort is as fast on average (O(n log n)) but sorts in place — using almost no extra memory. It's the most commonly used sorting algorithm in practice.

1. The Key Insight: Partition Without Merging

Mental Model

You have a group of students of varying heights. Pick one student as a pivot. Ask everyone shorter than the pivot to stand on the left, everyone taller on the right. Now the pivot is in its FINAL position. Recursively sort the left and right groups. Crucially: No merge step needed! The partitioning automatically puts elements in their final positions. (Diagram)

2. The Partition Algorithm

Mental Model

Scan the array with two markers. The lower marker tracks the boundary of elements ≤ pivot. The upper marker tracks the boundary of elements > pivot. Unclassified elements lie between them.
python
# runnable
def partition(arr, low, high):
    """Partition arr[low:high] around pivot (first element).
    Returns: index where pivot ends up.
    After partition:
    - Elements left of pivot are ≤ pivot
    - Elements right of pivot are > pivot
    """
    pivot = arr[low]
    lower = low + 1    # End of "lower than pivot" section
    upper = low + 1    # End of "upper than pivot" section
    for i in range(low + 1, high):
        if arr[i] > pivot:
            # Extend upper section
            upper += 1
        else:
            # Swap element to start of upper section
            arr[i], arr[lower] = arr[lower], arr[i]
            lower += 1
            upper += 1
    # Move pivot to its final position
    pivot_idx = lower - 1
    arr[low], arr[pivot_idx] = arr[pivot_idx], arr[low]
    return pivot_idx
# Test partition
arr = [43, 32, 22, 78, 63, 57, 91, 13]
idx = partition(arr, 0, len(arr))
print(f"After partition: {arr}")
print(f"Pivot index: {idx}, Pivot value: 43")
# [32, 22, 13, 43, 78, 63, 57, 91]
#                    ^^ pivot in final position

Step-by-Step Partition Trace

Partitioning [43, 32, 22, 78, 63, 57, 91, 13] with pivot = 43:
Steplowerupperiarr[i]ActionArray state
Init11[43, 32, 22, 78, 63, 57, 91, 13]
111132≤ pivot, swap with lower (32↔32)[43, 32, 22, 78, 63, 57, 91, 13]
222222≤ pivot, swap with lower (22↔22)[43, 32, 22, 78, 63, 57, 91, 13]
333378> pivot, extend upper[43, 32, 22, 78, 63, 57, 91, 13]
434463> pivot, extend upper[43, 32, 22, 78, 63, 57, 91, 13]
535557> pivot, extend upper[43, 32, 22, 78, 63, 57, 91, 13]
636691> pivot, extend upper[43, 32, 22, 78, 63, 57, 91, 13]
737713≤ pivot, swap with lower (13↔78)[43, 32, 22, 13, 63, 57, 91, 78]
FinalSwap pivot with lower-1[13, 32, 22, 43, 63, 57, 91, 78]

3. Full Quick Sort Implementation

python
# runnable
def quicksort(arr, low=0, high=None):
    """Sort arr[low:high] in-place using Quick Sort.
    Time: O(n log n) average, O(n²) worst
    Space: O(log n) for recursion stack
    Not stable
    """
    if high is None:
        high = len(arr)
    # Base case: 0 or 1 element
    if high - low <= 1:
        return arr
    # Partition and get pivot's final position
    pivot_idx = partition(arr, low, high)
    # Recursively sort left and right
    quicksort(arr, low, pivot_idx)
    quicksort(arr, pivot_idx + 1, high)
    return arr
# Test
arr = [43, 32, 22, 78, 63, 57, 91, 13]
result = quicksort(arr.copy())
print(f"Quick sorted: {result}")

Full Trace for [43, 32, 22, 78, 63, 57, 91, 13]

pseudo
quicksort([43, 32, 22, 78, 63, 57, 91, 13], 0, 8)
  ├── partition → pivot_idx=3
  │   arr becomes [13, 32, 22, 43, 63, 57, 91, 78]
  │
  ├── quicksort([13, 32, 22], 0, 3)     # Left of pivot
  │   ├── partition → pivot_idx=0 (pivot=13, all >13)
  │   │   arr unchanged: [13, 32, 22]
  │   ├── quicksort([], 0, 0) → base case
  │   └── quicksort([32, 22], 1, 3)
  │       ├── partition → pivot_idx=2 (pivot=32)
  │       │   arr becomes [13, 22, 32]
  │       └── (both sides base case)
  │
  └── quicksort([63, 57, 91, 78], 4, 8)  # Right of pivot
      ├── partition → pivot_idx=5 (pivot=63)
      │   arr becomes [13, 22, 32, 43, 57, 63, 91, 78]
      ├── quicksort([57], 4, 5) → base case
      └── quicksort([91, 78], 6, 8)
          ├── partition → pivot_idx=7 (pivot=91)
          │   arr becomes [13, 22, 32, 43, 57, 63, 78, 91]
          └── (both sides base case)

4. Pivot Selection Strategies

Why Pivot Choice Matters

The worst case for Quick Sort is when the pivot is always the smallest or largest element. This creates one subproblem of size 0 and another of size n-1, giving (T(n) = T(n-1) + n = O(n^2)). This happens when:
  • The array is already sorted (or reverse sorted), and
  • You always pick the first (or last) element as pivot.

Strategy 1: First/Last Element (Simple but Bad)

python
pivot = arr[low]  # Worst case: already sorted

Strategy 2: Random Pivot (Good in Practice)

python
import random
def partition_random(arr, low, high):
    """Partition using random pivot to avoid worst case."""
    rand_idx = random.randint(low, high - 1)
    arr[low], arr[rand_idx] = arr[rand_idx], arr[low]  # Swap pivot to front
    return partition(arr, low, high)
# Expected time: O(n log n) for ANY input

Strategy 3: Median of Three (Better Deterministic)

python
# runnable
def median_of_three(arr, low, high):
    """Use median of first, middle, and last as pivot."""
    mid = (low + high - 1) // 2
    # Sort the three candidates
    three = [(arr[low], low), (arr[mid], mid), (arr[high-1], high-1)]
    three.sort()
    median_idx = three[1][1]
    # Swap median to front
    arr[low], arr[median_idx] = arr[median_idx], arr[low]
    return partition(arr, low, high)
def quicksort_m3(arr, low=0, high=None):
    """Quick Sort with median-of-three pivot."""
    if high is None:
        high = len(arr)
    if high - low <= 1:
        return arr
    pivot_idx = median_of_three(arr, low, high)
    quicksort_m3(arr, low, pivot_idx)
    quicksort_m3(arr, pivot_idx + 1, high)
    return arr
arr = [1, 2, 3, 4, 5, 6, 7, 8]  # Already sorted!
result = quicksort_m3(arr.copy())
print(f"Median-of-3 sorted: {result}")  # Still fast!

5. Alternative Partition (Lomuto / Hoare)

Hoare Partition (Two Pointers from Ends)

python
# runnable
def partition_hoare(arr, low, high):
    """Hoare partition — meets in the middle. More efficient."""
    pivot = arr[low]
    i, j = low - 1, high
    while True:
        i += 1
        while arr[i] < pivot:
            i += 1
        j -= 1
        while arr[j] > pivot:
            j -= 1
        if i >= j:
            return j + 1  # Return pivot boundary
        arr[i], arr[j] = arr[j], arr[i]
def quicksort_hoare(arr, low=0, high=None):
    """Quick Sort using Hoare partition."""
    if high is None:
        high = len(arr)
    if high - low <= 1:
        return arr
    boundary = partition_hoare(arr, low, high)
    quicksort_hoare(arr, low, boundary)
    quicksort_hoare(arr, boundary, high)
    return arr
arr = [43, 32, 22, 78, 63, 57, 91, 13]
print(f"Hoare sorted: {quicksort_hoare(arr)}")

6. Complexity and Comparison

AspectValue
Best case(O(n \log n)) — pivot always divides evenly
Average case(O(n \log n)) — random pivot makes this typical
Worst case(O(n^2)) — pivot is always min/max
Space(O(\log n)) — recursion stack (in-place partition)
StableNo — partition swaps can reorder equal elements
In-placeYes — partition modifies the array directly

Quick Sort vs Merge Sort

ScenarioQuick SortMerge Sort
Large random data✅ Fastest in practice✅ Consistent
Already sorted (bad pivot)❌ O(n²)✅ O(n log n)
Memory constrained✅ In-place❌ O(n) extra space
Stable sort needed
Linked list❌ (needs random access)
Worst-case guarantee

Practice Questions

Q1. Trace partition on [9, 7, 5, 11, 12, 2, 14, 3, 10, 6] with pivot = first element. Q2. What is the worst-case input for Quick Sort when using first-element pivot? Why? Q3. How does random pivot selection eliminate the worst case? Q4. Why is Quick Sort not stable? Give a concrete example. Q5. After partitioning, the pivot is in its final position. Prove this. Q6. Show the recursion tree for Quick Sort on [1, 2, 3, 4, 5] with first-element pivot. Q7. How many partitioning levels occur in the best case for n = 16? Q8. Compare the constant factors: merge sort vs quick sort. Why is quick sort faster in practice despite the same average complexity? Q9. Implement an iterative version of Quick Sort using an explicit stack. Q10. What happens if we use the last element as pivot on a reverse-sorted list?
Answers
A1. Pivot = 9.
pseudo
Initial: [9, 7, 5, 11, 12, 2, 14, 3, 10, 6]
Step 1: [9, 7, 5, 11, 12, 2, 14, 3, 10, 6]  lower=1, upper=1, i=1: 7≤9
Step 2: [9, 7, 5, 11, 12, 2, 14, 3, 10, 6]  lower=2, upper=2, i=2: 5≤9
Step 3: [9, 7, 5, 11, 12, 2, 14, 3, 10, 6]  lower=2, upper=3, i=3: 11>9
Step 4: [9, 7, 5, 11, 12, 2, 14, 3, 10, 6]  lower=2, upper=4, i=4: 12>9
Step 5: [9, 7, 5, 2, 12, 11, 14, 3, 10, 6]  lower=3, upper=5, i=5: 2≤9, swap 2 with 11
Step 6: [9, 7, 5, 2, 12, 11, 14, 3, 10, 6]  lower=3, upper=6, i=6: 14>9
Step 7: [9, 7, 5, 2, 3, 11, 14, 12, 10, 6]  lower=4, upper=7, i=7: 3≤9, swap 3 with 12
Step 8: [9, 7, 5, 2, 3, 11, 14, 12, 10, 6]  lower=4, upper=8, i=8: 10>9
Step 9: [9, 7, 5, 2, 3, 6, 14, 12, 10, 11]  lower=5, upper=9, i=9: 6≤9, swap 6 with 11
Final: swap pivot with lower-1=4 → [6, 7, 5, 2, 3, 9, 14, 12, 10, 11]
A2. Already sorted array [1, 2, 3, 4, 5]. First element pivot = 1 is the smallest. Left partition = [], right partition = [2, 3, 4, 5]. Each recursive call reduces the problem by only 1 element → O(n²).
A3. Random pivot means the probability of consistently choosing the min/max element is vanishingly small ((2^{-n}) for n calls). The expected depth is O(log n).
A4. Partition swaps elements long distances — equal elements can cross each other. Example: [5, 3, 3'] with pivot 5. Partition sees 3 < 5 and 3' < 5, both go left. But if 3 and 3' were in different order before, the swap during partition can reverse them.
A5. After partition, all elements left of pivot are ≤ pivot, all right are > pivot. If we re-sorted the left side, the pivot would still be to the right of all of them. So the pivot is in its unique correct position.
A6. Each level has one subproblem of size n-1 instead of splitting. The tree is a linear chain of depth n (degenerate).
A7. Best case: pivot always splits exactly in half. Levels = log₂(16) = 4.
A8. Quick Sort has better cache locality (sequential access), no extra memory allocation, and handles small subproblems quickly. Merge Sort allocates many temporary arrays and copies data.
A9.
python
def quicksort_iterative(arr):
    stack = [(0, len(arr))]
    while stack:
        low, high = stack.pop()
        if high - low <= 1:
            continue
        pivot_idx = partition(arr, low, high)
        stack.append((low, pivot_idx))
        stack.append((pivot_idx + 1, high))
    return arr
A10. [5, 4, 3, 2, 1] with last-element pivot = 1 (the smallest) → worst case O(n²). Same problem as first-element on already sorted. Join Discord Previous06. Merge Sort — The O(n log n) BreakthroughNext08. Sorting Algorithms Comparison — Heap Sort, Counting Sort, Radix Sort
Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.