Dutch National Flag and In-place Partitioning
Sorting an array of three distinct values is the shortest problem in this phase with a genuinely subtle answer. Counting sort does it in two passes and is a perfectly good answer. The interviewer will then ask for one pass, and that is where the Dutch national flag algorithm comes in — named by Dijkstra after the Dutch flag’s three horizontal bands.
It is worth learning for a reason beyond LeetCode 75: three-way partitioning is what makes quicksort survive an array of mostly-equal values, which is the input that turns naive quicksort quadratic.
What you’ll learn
Section titled “What you’ll learn”- The three-region invariant and why maintaining it needs exactly three pointers.
- Why
midadvances on a0and a1but not on a2— the one line that decides correctness. - Why three-way partitioning fixes quicksort’s worst case on duplicate-heavy input.
- The variants: partition around a pivot, move zeroes, sort by parity.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”Watch the unknown region collapse from both ends, and watch what mid does on a
2:
On a 2 the value swapped in from the right has not been examined yet, so mid must stay put. Advancing there is the defining bug: it passes many inputs and silently leaves a 2 stranded in the middle.
The template
Section titled “The template”def sort_colors(arr): # LC 75
low, mid, high = 0, 0, len(arr) - 1
while mid <= high: # <= , not < : high is inclusive
if arr[mid] == 0:
arr[low], arr[mid] = arr[mid], arr[low]
low += 1
mid += 1 # swapped-in value is a known 1
elif arr[mid] == 1:
mid += 1 # already in the right region
else: # == 2
arr[mid], arr[high] = arr[high], arr[mid]
high -= 1 # mid does NOT advance
return arr
def three_way_partition(arr, pivot): # the quicksort building block
low, mid, high = 0, 0, len(arr) - 1
while mid <= high:
if arr[mid] < pivot:
arr[low], arr[mid] = arr[mid], arr[low]
low += 1
mid += 1
elif arr[mid] == pivot:
mid += 1
else:
arr[mid], arr[high] = arr[high], arr[mid]
high -= 1
return low, high # arr[low..high] all == pivot
print(sort_colors([2, 0, 2, 1, 1, 0])) # expect [0, 0, 1, 1, 2, 2]
print(three_way_partition([3, 1, 3, 5, 3, 2], 3)) # expect (2, 3)Dry run
Section titled “Dry run”arr = [2, 0, 2, 1, 1, 0]. The action column is what to narrate aloud.
| step | array | low | mid | high | arr[mid] | action |
|---|---|---|---|---|---|---|
| 1 | [2,0,2,1,1,0] | 0 | 0 | 5 | 2 | swap mid↔high; high→4; mid stays |
| 2 | [0,0,2,1,1,2] | 0 | 0 | 4 | 0 | swap mid↔low; low→1, mid→1 |
| 3 | [0,0,2,1,1,2] | 1 | 1 | 4 | 0 | swap mid↔low; low→2, mid→2 |
| 4 | [0,0,2,1,1,2] | 2 | 2 | 4 | 2 | swap mid↔high; high→3; mid stays |
| 5 | [0,0,1,1,2,2] | 2 | 2 | 3 | 1 | mid→3 |
| 6 | [0,0,1,1,2,2] | 2 | 3 | 3 | 1 | mid→4 |
| 7 | — | 2 | 4 | 3 | — | mid > high, loop ends |
Result [0, 0, 1, 1, 2, 2]. Six elements, seven iterations, and every element
moved at most twice.
Read step 1 and step 4 together. Both saw a 2 and both left mid where it was.
At step 1 the value swapped in was a 0 — still unexamined. Advance mid there
and that 0 is stranded to the right of low forever, and the output is
[2,0,...]-shaped garbage.
Complexity
Section titled “Complexity”| Approach | Passes | Time | Space |
|---|---|---|---|
Built-in sort() | — | (Timsort) | |
| Counting sort | 2 | — three counters | |
| Dutch national flag | 1 |
Each element is swapped at most twice — once into the 0 region, once into the
2 region — so the constant factor is small. Counting sort is equally and
much easier to write; the only thing DNF buys is the single pass. Say that
plainly, and offer counting sort first if the problem does not demand one pass —
volunteering the simpler correct answer before the clever one reads well.
The variant map
Section titled “The variant map”| Variant | Change | Canonical problem |
|---|---|---|
| Sort 0/1/2 | the base template | 75 Sort Colors |
| Partition around a pivot | compare < pivot, == pivot, > pivot | quicksort with duplicates |
| Two categories only | one write pointer, no high | 283 Move Zeroes · 905 Sort By Parity |
| Move zeroes to the end, order preserved | two pointers, no third region | 283 Move Zeroes |
| Wiggle / rearrange | the same partition then interleave | 324 Wiggle Sort II |
| Quickselect with duplicates | recurse only into the < or > region | 215 Kth Largest |
Pitfalls
Section titled “Pitfalls”- Advancing
midafter a2swap. The single most common failure. The incoming value is unexamined. while mid < high.highis inclusive; the final element is skipped and the bug appears only on certain inputs.- Swapping with
lowand then not advancingmid. The opposite error — this causes an infinite loop whenlow == mid. - Using it for more than three categories. The invariant does not generalise; use counting sort.
- Reaching for it when two categories suffice. Move Zeroes needs one write pointer, not three. The extra pointer is complexity for nothing.
- Not offering counting sort first. If the problem does not demand one pass, the two-pass version is simpler and equally optimal — and saying so is a signal, not a weakness.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
“Why does mid not advance on a 2?” | Whether you understand or memorised | The value swapped in from high has never been examined. Advancing strands it |
“Then why does it advance on a 0?” | Depth of the invariant | The value swapped in from low is necessarily a 1, because low..mid-1 holds only 1s. It needs no re-examination |
| “Can you do it in two passes?” | Whether you know the simpler answer | Counting sort: count the three values, then overwrite. Same , far easier, and the right answer unless one pass is required |
“Generalise to k categories” | Boundaries | The invariant does not extend past three. Use counting sort — |
| “How does this help quicksort?” | Whether you see the connection | Three-way partitioning puts equal values in the middle and recurses only into the outer regions, so an all-equal array is sorted in one partition instead of degrading to |
| “Is it stable?” | Precision | No. Swapping across the array reorders equal elements. If stability matters, use a stable sort or an auxiliary array |
Practice
Section titled “Practice”Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 283Move Zeroeseasy
- 75Sort Colorsmedium
- 324Wiggle Sort IImedium
Exercises
Section titled “Exercises”LC 75 — Sort Colors · Medium
Section titled “LC 75 — Sort Colors · Medium”Three-way partition around a pivot
Section titled “Three-way partition around a pivot”LC 283 — Move Zeroes · Easy (the two-category case)
Section titled “LC 283 — Move Zeroes · Easy (the two-category case)”Self-check
Section titled “Self-check”-
Why does `mid` not advance after swapping a 2 to the right?
This is the defining detail. Advancing here passes many inputs and silently leaves an unexamined value in the middle region, so the failure is intermittent and hard to spot.
pch.quizShowAnswer
B — Because the value swapped in from `high` has never been examined — advancing would strand it — This is the defining detail. Advancing here passes many inputs and silently leaves an unexamined value in the middle region, so the failure is intermittent and hard to spot.
-
Then why DOES `mid` advance after swapping a 0 to the left?
The asymmetry follows from the invariant rather than being arbitrary. Being able to derive it is what separates understanding the algorithm from having memorised it.
pch.quizShowAnswer
B — Because the value swapped in from `low` is necessarily a 1 — everything in low..mid-1 is a 1 by the invariant — so it needs no re-examination — The asymmetry follows from the invariant rather than being arbitrary. Being able to derive it is what separates understanding the algorithm from having memorised it.
-
Why is the loop condition `mid <= high` rather than `mid < high`?
Boundary convention decides the comparison. With an inclusive `high`, `<` leaves one element unprocessed, and the bug shows only on inputs where that element is out of place.
pch.quizShowAnswer
B — Because `high` is an inclusive boundary — the element at `high` still needs examining, and `<` would skip the last element — Boundary convention decides the comparison. With an inclusive `high`, `<` leaves one element unprocessed, and the bug shows only on inputs where that element is out of place.
-
The interviewer has not asked for one pass. What do you offer first?
Volunteering the simpler correct answer before the clever one reads as judgement, not weakness. DNF's only advantage is the single pass, so it should be justified by a stated requirement.
pch.quizShowAnswer
B — Counting sort — two passes, same O(n) time, far simpler and equally correct — then mention DNF if one pass is required — Volunteering the simpler correct answer before the clever one reads as judgement, not weakness. DNF's only advantage is the single pass, so it should be justified by a stated requirement.
-
How does three-way partitioning fix quicksort on duplicate-heavy input?
Standard Lomuto splits into < and >=, so an all-equal array sends everything to one side and the recursion depth becomes n. This is why production quicksorts partition three ways.
pch.quizShowAnswer
B — It puts all copies of the pivot in the middle and recurses only into the outer regions, so an all-equal array is sorted in one partition step rather than degrading to O(n squared) — Standard Lomuto splits into < and >=, so an all-equal array sends everything to one side and the recursion depth becomes n. This is why production quicksorts partition three ways.
Recall card
Section titled “Recall card”- Cue — exactly three categories, one pass, space. Two categories need only one write pointer; more than three need counting sort.
- Invariant —
[0, low)all lows ·[low, mid)all middles ·[mid, high]unknown ·(high, n)all highs. - Template —
while mid <= high: on low, swap withlowand advance both; on middle, advancemid; on high, swap withhigh, decrementhigh, and leavemid. - The three breakages —
<instead of<=; advancingmidon a high; not advancingmidon a low. - Complexity — , one pass, at most two swaps per element, not stable.
- Offer counting sort first unless one pass is required.
- Quicksort connection — three-way partitioning is what stops duplicate-heavy input degrading to .
- Three-way partitioning maintains four regions and shrinks the unknown one to nothing in a single pass.
- The whole algorithm is the invariant: state the four regions and the branches
follow, including why
midadvances on a low but not on a high. - Counting sort is equally optimal and simpler; DNF’s only advantage is the single pass, so justify it against a stated requirement.
- The same partition makes quicksort robust against arrays of mostly-equal values, which is its real-world use.
Next: Trie Patterns — prefix trees applied to grid search and wildcard matching.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading