07. Quick Sort — The Fastest Practical Sort
1892 words
9 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 07. Quick Sort — The Fastest Practical Sort > **What problem does this solve?** Merge sort is fast but uses O(n) extra memory.

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:
| Step | lower | upper | i | arr[i] | Action | Array state |
|---|---|---|---|---|---|---|
| Init | 1 | 1 | — | — | — | [43, 32, 22, 78, 63, 57, 91, 13] |
| 1 | 1 | 1 | 1 | 32 | ≤ pivot, swap with lower (32↔32) | [43, 32, 22, 78, 63, 57, 91, 13] |
| 2 | 2 | 2 | 2 | 22 | ≤ pivot, swap with lower (22↔22) | [43, 32, 22, 78, 63, 57, 91, 13] |
| 3 | 3 | 3 | 3 | 78 | > pivot, extend upper | [43, 32, 22, 78, 63, 57, 91, 13] |
| 4 | 3 | 4 | 4 | 63 | > pivot, extend upper | [43, 32, 22, 78, 63, 57, 91, 13] |
| 5 | 3 | 5 | 5 | 57 | > pivot, extend upper | [43, 32, 22, 78, 63, 57, 91, 13] |
| 6 | 3 | 6 | 6 | 91 | > pivot, extend upper | [43, 32, 22, 78, 63, 57, 91, 13] |
| 7 | 3 | 7 | 7 | 13 | ≤ pivot, swap with lower (13↔78) | [43, 32, 22, 13, 63, 57, 91, 78] |
| Final | — | — | — | — | Swap 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]
pseudoquicksort([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)
pythonpivot = arr[low] # Worst case: already sorted
Strategy 2: Random Pivot (Good in Practice)
pythonimport 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
| Aspect | Value |
|---|---|
| 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) |
| Stable | No — partition swaps can reorder equal elements |
| In-place | Yes — partition modifies the array directly |
Quick Sort vs Merge Sort
| Scenario | Quick Sort | Merge 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?
AnswersA1. Pivot = 9.pseudoInitial: [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.pythondef 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 arrA10. [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