Data Structure 📂 Sorting & Order Statistics · 4 of 10 35 min read

Quicksort, Visualized: A Hands-On Divide-and-Conquer Guide

A practical, hands-on quicksort tutorial built around an interactive animation that highlights each line of Python while showing the Lomuto partition — moving i/j pointers, the pivot, and the recursion window — in lock-step. Covers divide-and-conquer, pivot selection, why the worst case is O(n²), real benefits and use cases, and how it compares to merge, heap, and insertion sort.

Section 01

The Story That Makes Quicksort Click

🎥 The Movie-Night Height Line-Up
A crowd needs to line up by height for a photo. Instead of carefully inserting people one by one, you grab one person at random — call them the pivot — and shout: "Everyone shorter than this person, stand to the left; everyone taller, stand to the right." In one sweep the crowd splits into two rough groups, and the pivot is now standing in exactly the right spot — permanently.

Now you have two smaller crowds. You repeat the same trick inside each group: pick a pivot, split left and right, lock the pivot in. Keep going on ever-smaller groups until every group has one person — and the whole line is sorted.

That is Quicksort: partition around a pivot, then recursively quicksort the two halves. Divide, conquer, done.

Quicksort is a divide-and-conquer algorithm. Each partition step picks a pivot and rearranges the current section so that everything smaller sits to its left and everything larger to its right. The pivot then lands in its final position, and the algorithm recurses on the left and right sections independently.

🧠
The Core Insight

One partition pass does not sort the section — it just guarantees the pivot is home and cleanly separates "smaller" from "larger." That single guarantee, applied recursively, is enough to sort everything in O(n log n) on average, fully in place, which is why quicksort is the fastest general-purpose sort in practice.


Section 02

Divide and Conquer — One Pivot, Two Sub-Problems

Every partition turns one problem into two smaller, independent ones. The pivot is removed from play (it is already final), and quicksort is called again on each side.

Unsorted section [low … high] partition(pivot) < pivot pivot > pivot pivot is now in its FINAL position — never moved again quicksort(left) quicksort(right) … recurse until each section has 0 or 1 element (already sorted)
✂️
Why "In Place" Matters

Unlike Merge Sort, quicksort does its splitting by swapping elements within the same array — no auxiliary array needed. Only the recursion call stack uses extra space (O(log n) on average). That, plus excellent CPU-cache behaviour, is why quicksort usually beats merge sort and heapsort on real hardware.


Section 03

The Partition Step (Lomuto Scheme)

The animation below uses the Lomuto partition: the pivot is the last element, a boundary pointer i marks the end of the "smaller than pivot" zone, and a scanner j walks across the section.

🎯 HOW LOMUTO PARTITION WORKS
1
Choose the last element as the pivot. Set i = low - 1 (the smaller-zone is empty).
2
Scan with j from low to high - 1. Compare each element to the pivot.
3
If arr[j] < pivot, grow the smaller-zone: i += 1, then swap arr[i] with arr[j].
4
After the scan, swap the pivot into position i + 1 — right after the smaller-zone. That index is its final home.

Section 04

Hands-On: Watch Every Line of Code Execute

Press Play (or step manually). The highlighted Python line moves in lock-step with the animated array. The amber i and blue j pointers track the partition; the purple bar is the pivot; the active recursion window stays bright while inactive sub-sections dim. Each time a pivot locks into place it turns green permanently. Use Shuffle for a fresh array.

⚡ LIVE TRACE — THIS LINE IS DOING THIS TASK
window [0 .. 6]
1def partition(arr, low, high):
2 pivot = arr[high]
3 i = low - 1
4 for j in range(low, high):
5 if arr[j] < pivot:
6 i += 1
7 arr[i], arr[j] = arr[j], arr[i]
8 arr[i + 1], arr[high] = arr[high], arr[i + 1]
9 return i + 1
 
11def quick_sort(arr, low, high):
12 if low < high:
13 p = partition(arr, low, high)
14 quick_sort(arr, low, p - 1)
15 quick_sort(arr, p + 1, high)
16 return arr
Press Play or Step ▶ to begin.
Speed Step 0 / 0
👀
How to Read the Animation

The blue j sweeps the window comparing each value to the purple pivot. Whenever a value is smaller, the amber i advances and a red swap pulls that value into the growing "< pivot" zone (the amber-underlined bars). At the end of the scan, the pivot swaps to just past that zone and turns green. Then quicksort dives into the left window, then the right.


Section 05

The Full Python Implementation

def partition(arr, low, high):
    pivot = arr[high]          # choose the last element as pivot
    i = low - 1              # end of the "< pivot" zone

    for j in range(low, high):
        if arr[j] < pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]   # move smaller value left

    arr[i + 1], arr[high] = arr[high], arr[i + 1]  # pivot into place
    return i + 1


def quick_sort(arr, low, high):
    if low < high:
        p = partition(arr, low, high)
        quick_sort(arr, low, p - 1)   # sort left of pivot
        quick_sort(arr, p + 1, high)  # sort right of pivot
    return arr


# --- driver ---
data = [5, 3, 8, 1, 9, 2, 7]
print("Before:", data)
quick_sort(data, 0, len(data) - 1)
print("After: ", data)
OUTPUT
Before: [5, 3, 8, 1, 9, 2, 7] After: [1, 2, 3, 5, 7, 8, 9]

A Verbose Version That Shows Each Pivot Landing

def quick_sort_verbose(arr, low, high):
    if low < high:
        p = partition(arr, low, high)
        print(f"pivot {arr[p]:>2} -> final index {p}   {arr}")
        quick_sort_verbose(arr, low, p - 1)
        quick_sort_verbose(arr, p + 1, high)
    return arr

quick_sort_verbose([5, 3, 8, 1, 9, 2, 7], 0, 6)
OUTPUT
pivot 7 -> final index 4 [5, 3, 1, 2, 7, 8, 9] pivot 2 -> final index 1 [1, 2, 5, 3, 7, 8, 9] pivot 3 -> final index 2 [1, 2, 3, 5, 7, 8, 9] pivot 9 -> final index 6 [1, 2, 3, 5, 7, 8, 9]
Notice the Order

The first pivot (7) lands at index 4, splitting the work. Quicksort then fully sorts the left window before touching the right — that is the recursion unfolding depth-first. Each printed line is one pivot reaching its permanent home.


Section 06

Why Use Quicksort? The Real Benefits

🚀
Fastest in Practice
great cache locality
On average O(n log n) with a very small constant factor. Its sequential, in-place memory access plays beautifully with CPU caches — typically 2–3× faster than heapsort and often faster than merge sort.
📐
In-Place, Low Memory
O(log n) stack only
Partitioning happens by swapping inside the original array — no O(n) auxiliary buffer like merge sort needs. The only extra space is the recursion stack, O(log n) on average.
🧩
Tunable & Foundational
pivot strategies
Pivot choice (random, median-of-three) lets you dodge the worst case. It underpins real library sorts — std::sort, introsort, and dual-pivot quicksort in Java.
🔌
Why Real Libraries Love It

Most standard-library sorts are quicksort-based hybrids. Introsort runs quicksort but watches the recursion depth; if a bad pivot streak threatens O(n²), it switches to heapsort to guarantee O(n log n), and uses insertion sort for tiny sub-arrays. Best of all worlds.


Section 07

When to Use It — and When Not To

Large In-Memory Arrays
General-purpose sorting of big arrays that fit in RAM — databases, search engines, analytics. Its average speed is hard to beat.
default fast sort
Memory Is Tight
When an O(n) merge buffer is too costly, quicksort's in-place partitioning keeps the footprint minimal.
in-place
Average Speed Matters Most
With randomised or median-of-three pivots, the O(n²) worst case becomes vanishingly unlikely, so you get reliable speed.
randomised pivot
Hard Worst-Case Guarantees
A naive pivot on sorted/reverse data degrades to O(n²). If you must guarantee O(n log n), use heapsort or merge sort.
use heapsort/merge
Stability Required
Quicksort is not stable — far-apart swaps reorder equal keys. For stable sorting, choose merge sort.
equal-key order matters
Tiny or Linked Data
For very small arrays, insertion sort is faster; for linked lists with no random access, merge sort fits better.
prefer insertion/merge

Section 08

Time & Space Complexity

CaseWhen It HappensTimeSpace (stack)
BestPivot splits the section evenlyO(n log n)O(log n)
AverageRandom data, reasonable pivotsO(n log n)O(log n)
WorstPivot is always min/max (e.g. sorted input, last-element pivot)O(n²)O(n)
Stable?Far-apart swaps reorder equal keysNo (not stable)
⚠️
The Worst Case Is About the Pivot, Not the Data

If every pivot is the smallest or largest element, each partition peels off just one element and the recursion becomes n levels deep — that is the O(n²) trap, and ironically it hits already-sorted input when you always pick the last element. A random or median-of-three pivot makes this practically impossible.


Section 09

Choosing a Good Pivot

⚠️
First / Last Element
Simplest to code, but predictable — sorted or reverse-sorted input triggers the O(n²) worst case every time.
risky on ordered data
🎲
Random Element
Pick a random index as pivot. No specific input can reliably force the worst case, so expected time stays O(n log n).
recommended default
🎯
Median-of-Three
Take the median of first, middle, and last. Produces more balanced splits, especially on partially-ordered data.
great in practice

Section 10

Quicksort vs Its Cousins

PropertyQuicksortMerge SortHeapsortInsertion Sort
AverageO(n log n)O(n log n)O(n log n)O(n²)
Worst caseO(n²)O(n log n)O(n log n)O(n²)
Extra spaceO(log n)O(n)O(1)O(1)
Stable?NoYesNoYes
In practiceUsually fastestFast, needs memorySlower (cache misses)Great when tiny
Best for…General-purpose speedStable / linked listsWorst-case + low memorySmall / nearly-sorted
🏆
The Practitioner's Take

Reach for quicksort as your default for sorting large in-memory arrays where stability is not required — just use a randomised or median-of-three pivot. When you need a guaranteed bound, stability, or are sorting linked/external data, switch to heapsort or merge sort instead.


Section 11

Golden Rules

🎯 Quicksort — Things Worth Memorising
1
A partition does not sort — it places one pivot in its final spot and splits the rest into "< pivot" and "> pivot." Sorting comes from recursing on both sides.
2
In Lomuto, i is the boundary of the smaller-zone and j is the scanner. The pivot ends at i + 1 after the loop — never forget that final swap.
3
The base case is low < high. Sections of size 0 or 1 are already sorted, so the recursion simply stops there.
4
Pivot choice decides everything. Use a random or median-of-three pivot to keep the average O(n log n) and avoid the O(n²) worst case on ordered data.
5
Quicksort is not stable. If equal keys must keep their original order, use merge sort instead.
6
Recurse into the smaller side first (and loop on the larger) to cap stack depth at O(log n). Production sorts also fall back to insertion sort for tiny sections.