Skip to content

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.

  • The three-region invariant and why maintaining it needs exactly three pointers.
  • Why mid advances on a 0 and a 1 but not on a 2 — 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.

Watch the unknown region collapse from both ends, and watch what mid does on a 2:

arrayThree regions, one pass, and one pointer that does not always advanceLC 75 · O(n) time, O(1) space
unknown
200122131405
lowmidhigh
setupThree regions, one pass: everything left of low is 0, everything right of high is 2, and low..high is still unknown. The loop shrinks the unknown region to nothing.
1/8

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.

dutch_national_flag.py
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)

arr = [2, 0, 2, 1, 1, 0]. The action column is what to narrate aloud.

steparraylowmidhigharr[mid]action
1[2,0,2,1,1,0]0052swap mid↔high; high→4; mid stays
2[0,0,2,1,1,2]0040swap mid↔low; low→1, mid→1
3[0,0,2,1,1,2]1140swap mid↔low; low→2, mid→2
4[0,0,2,1,1,2]2242swap mid↔high; high→3; mid stays
5[0,0,1,1,2,2]2231mid→3
6[0,0,1,1,2,2]2331mid→4
7243mid > 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.

ApproachPassesTimeSpace
Built-in sort()O(nlogn)O(n \log n)O(n)O(n) (Timsort)
Counting sort2O(n)O(n)O(1)O(1) — three counters
Dutch national flag1O(n)O(n)O(1)O(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 O(n)O(n) 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.

VariantChangeCanonical problem
Sort 0/1/2the base template75 Sort Colors
Partition around a pivotcompare < pivot, == pivot, > pivotquicksort with duplicates
Two categories onlyone write pointer, no high283 Move Zeroes · 905 Sort By Parity
Move zeroes to the end, order preservedtwo pointers, no third region283 Move Zeroes
Wiggle / rearrangethe same partition then interleave324 Wiggle Sort II
Quickselect with duplicatesrecurse only into the < or > region215 Kth Largest
  • Advancing mid after a 2 swap. The single most common failure. The incoming value is unexamined.
  • while mid < high. high is inclusive; the final element is skipped and the bug appears only on certain inputs.
  • Swapping with low and then not advancing mid. The opposite error — this causes an infinite loop when low == 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.
They askWhat they’re checkingThe answer
“Why does mid not advance on a 2?”Whether you understand or memorisedThe value swapped in from high has never been examined. Advancing strands it
“Then why does it advance on a 0?”Depth of the invariantThe 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 answerCounting sort: count the three values, then overwrite. Same O(n)O(n), far easier, and the right answer unless one pass is required
“Generalise to k categories”BoundariesThe invariant does not extend past three. Use counting sort — O(n+k)O(n + k)
“How does this help quicksort?”Whether you see the connectionThree-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 O(n2)O(n^2)
“Is it stable?”PrecisionNo. Swapping across the array reorders equal elements. If stability matters, use a stable sort or an auxiliary array
3 problems
1 easy2 medium0 hard

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.

LC 283 — Move Zeroes · Easy (the two-category case)

Section titled “LC 283 — Move Zeroes · Easy (the two-category case)”
pch.quizTag Dutch national flag — self-check
  1. Why does `mid` not advance after swapping a 2 to the right?

    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.

  2. Then why DOES `mid` advance after swapping a 0 to the left?

    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.

  3. Why is the loop condition `mid <= high` rather than `mid < high`?

    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.

  4. The interviewer has not asked for one pass. What do you offer first?

    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.

  5. How does three-way partitioning fix quicksort on duplicate-heavy input?

    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.

  • Cue — exactly three categories, one pass, O(1)O(1) 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.
  • Templatewhile mid <= high: on low, swap with low and advance both; on middle, advance mid; on high, swap with high, decrement high, and leave mid.
  • The three breakages< instead of <=; advancing mid on a high; not advancing mid on a low.
  • ComplexityO(n)O(n), 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 O(n2)O(n^2).
  • 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 mid advances 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.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading