Learn/DSA Patterns
DSA PatternsArrays & Two Pointerseasy15 min read

Dutch National Flag — Three-Way Partition

Sort an array of three distinct values in a single pass and O(1) space using three pointers. We build the idea from a sorting-laundry story, prove the invariant, and apply it to five real problems.

#two-pointers#three-pointers#partition#sorting#beginner#interview
Table of contents

Before we start

In the last chapter we used a slow + fast pointer to separate keepers from junk. This chapter extends that to three zones — low, middle, high — using three pointers. By the end you will be able to:

  • See the three-pointer partition as a concrete sorting action, like hands sorting laundry.
  • Explain out loud why the algorithm is correct even though it processes elements in a scrambled order.
  • Recognise this pattern in problems about 0s/1s/2s, two-part partitions, and flag-style sorting.

Stop at every Pause & Think box before reading on.


Picture this first (no code yet)

A real-life story

It's laundry day. You have a large pile of clothes mixed randomly on the floor. Every item is one of three colours: red, white, or blue — the colours of the Dutch flag (that's where the algorithm gets its name).

Your goal: sort them into three neat sections without picking up more than one item at a time, and without using a second pile or a table. Everything has to stay on the same floor space.

You mark two lines on the floor:

  • A left boundary — everything to the left of it is red (already sorted).
  • A right boundary — everything to the right of it is blue (already sorted).
  • Everything between the two boundaries is the unsorted mess you're still working through.

You stand at the left edge of the unsorted middle. You pick up one item:

  • Red? Swap it with the item just past the left boundary, extend the left boundary rightward. You've grown the red section by one.
  • Blue? Swap it with the item just inside the right boundary, pull the right boundary leftward. You've grown the blue section by one. Don't advance your position — the item you just swapped in from the right is unseen.
  • White? It belongs in the middle — just move on.

You keep going until your position meets the right boundary. At that point, every item is in its correct section. Done. One pass. Zero extra space.

That laundry sorting is the Dutch National Flag algorithm. The left boundary is the low pointer, the right boundary is high, and your current position is mid.


The actual problem

You have an array containing only the values 0, 1, and 2 (in any order). Sort it in-place, using one pass, using O(1) extra space. After sorting: all 0s come first, then all 1s, then all 2s.

Example:

input  : [2, 0, 2, 1, 1, 0]
output : [0, 0, 1, 1, 2, 2]

The mapping to laundry: 0 = red, 1 = white, 2 = blue.


First, the slow way (so you feel the pain)

Approach 1 — count and rebuild:

Count how many 0s, 1s, and 2s exist, then overwrite the array:

from collections import Counter

def sort_colors_count(nums):
    c = Counter(nums)
    i = 0
    for val in [0, 1, 2]:
        for _ in range(c[val]):
            nums[i] = val
            i += 1

This works. Two passes — one to count, one to write. Time O(n), space O(1).

Approach 2 — general comparison sort:

nums.sort()   # O(n log n) — completely ignores the three-value structure

Wasteful. We know there are only three distinct values. A comparison sort does n log n comparisons when n comparisons suffice.

The Dutch National Flag algorithm does it in one pass, O(n) time, O(1) space — twice as fast as counting and infinitely faster than general sort.


The turning point

Think about the counting approach. Why does it need two passes? Because on the first pass it only reads — it doesn't know where to put things until the counts are complete.

Pause & think

Is there a way to sort without knowing the counts in advance? Imagine you're sorting a new card from the pile one at a time. What would you need to know — at all times — to place each card correctly without revisiting earlier cards?

Here's the insight. You don't need to know how many of each value exists. You only need to maintain one fact at every moment:

"Everything to the left of low is a 0. Everything to the right of high is a 2. Everything between low and mid is a 1."

As long as that statement is always true, when mid and high finally meet, the whole array is sorted. You just need to make every new element you examine immediately true to that statement.


The one idea to remember

The entire pattern in one sentence

Use three pointers — low (end of 0s), mid (current element), high (start of 2s) — and with each element you look at, swap it to its correct boundary and shrink the unsorted middle until mid crosses high.


Watch it happen, frame by frame

Array: [2, 0, 2, 1, 1, 0]

We start with low = 0, mid = 0, high = 5.

State:  low=0  mid=0  high=5
Array: [2, 0, 2, 1, 1, 0]
        ↑                ↑
       mid             high
       low

mid element = 2  →  BLUE → swap nums[mid] with nums[high], high--
Array: [0, 0, 2, 1, 1, 2]   (high shrinks to 4, mid stays at 0)
State:  low=0  mid=0  high=4
Array: [0, 0, 2, 1, 1, 2]
        ↑             ↑
       mid           high
       low

mid element = 0  →  RED → swap nums[mid] with nums[low], low++, mid++
Array: [0, 0, 2, 1, 1, 2]   (self-swap, both advance)
low=1, mid=1
State:  low=1  mid=1  high=4
Array: [0, 0, 2, 1, 1, 2]

mid element = 0  →  RED → swap nums[1] with nums[1], low=2, mid=2
Array unchanged.
State:  low=2  mid=2  high=4
Array: [0, 0, 2, 1, 1, 2]
              ↑       ↑
             mid     high

mid element = 2  →  BLUE → swap nums[mid] with nums[high], high--
Array: [0, 0, 1, 1, 2, 2]   high=3
State:  low=2  mid=2  high=3
Array: [0, 0, 1, 1, 2, 2]
              ↑    ↑
             mid  high

mid element = 1  →  WHITE → mid++
mid=3
State:  low=2  mid=3  high=3
Array: [0, 0, 1, 1, 2, 2]mid=high

mid element = 1  →  WHITE → mid++
mid=4  >  high=3  →  STOP
Final: [0, 0, 1, 1, 2, 2]

Pause & think

Cover the trace above. Try [1, 2, 0, 1, 2] yourself. What are low, mid, high after each step? What is the final array?

Check your trace
low=0, mid=0, high=4
mid=1 (white) → mid=1
mid=2 (blue)  → swap(1,4), high=3. Array:[1,2,0,1,2]→[1,2,0,1,2] wait:
  swap nums[1] and nums[4]: [1,2,0,1,2]. Hmm, let's be careful.
  nums[mid]=nums[0]=1 → white → mid++

low=0, mid=1, high=4
nums[mid]=nums[1]=2 → blue → swap(nums[1],nums[4]), high=3
Array: [1,2,0,1,2] → swap index 1 and 4 → [1,2,0,1,2] ... nums[4]=2, nums[1]=2, same → [1,2,0,1,2], high=3

low=0, mid=1, high=3
nums[mid]=nums[1]=2 → blue → swap(nums[1],nums[3]), high=2
Array: [1,1,0,2,2], high=2

low=0, mid=1, high=2
nums[mid]=nums[1]=1 → white → mid=2

low=0, mid=2, high=2
nums[mid]=nums[2]=0 → red → swap(nums[0],nums[2]), low=1, mid=3
Array: [0,1,1,2,2]

mid=3 > high=2 → STOP
Final: [0, 1, 1, 2, 2]

Now, the code — line by line

def sort_colors(nums):
    low = 0               # boundary: everything left of low is a 0
    mid = 0               # current element being examined
    high = len(nums) - 1  # boundary: everything right of high is a 2

    while mid <= high:    # unsorted middle exists while mid hasn't crossed high
        if nums[mid] == 0:
            nums[low], nums[mid] = nums[mid], nums[low]  # red → send to left section
            low += 1      # left boundary grows
            mid += 1      # element at new mid was a known 1 (from left section), so advance
        elif nums[mid] == 2:
            nums[mid], nums[high] = nums[high], nums[mid]  # blue → send to right section
            high -= 1     # right boundary shrinks
            # DO NOT advance mid — the swapped-in element is unseen
        else:
            mid += 1      # white (1) → already in the right place, just move on

Every line mapped to the laundry story:

  • low = 0 — the left boundary of the sorted red pile starts empty at position 0.
  • high = len(nums) - 1 — the right boundary of the sorted blue pile starts empty at the last position.
  • mid = 0 — you stand at the very first item in the unsorted pile.
  • while mid <= high: — keep sorting while there's still unsorted middle ground between your hands.
  • nums[mid] == 0 → swap to the left, push both boundaries inward. The item that was at low was a 1 (proven by the invariant), so mid can safely advance.
  • nums[mid] == 2 → swap to the right, pull the right boundary in. Do not advance mid — the item that just arrived from position high has never been examined.
  • else (it's a 1) → it belongs in the middle; just step over it.

Why does it never go wrong?

The algorithm stays correct because of one invariant it maintains at all times:

[ all 0s | all 1s | UNSORTED | all 2s ]
  0..low-1  low..mid-1  mid..high  high+1..n-1
  • Everything left of low is guaranteed to be a 0.
  • Everything between low and mid is guaranteed to be a 1.
  • Everything between mid and high (inclusive) is unknown.
  • Everything right of high is guaranteed to be a 2.

Every operation preserves this invariant:

  • Swap a 0 to the left: low region gains a 0, the 1 displaced goes to the middle region, mid advances. ✓
  • Swap a 2 to the right: high region gains a 2, the swapped-in element is unknown so it joins the unsorted middle. ✓
  • Skip a 1: it was already in the middle region. ✓

When mid > high, the unsorted region is empty. The invariant then covers the entire array — it's perfectly sorted. □

The critical detail

When you swap a 2 to the right, do not advance mid. The item that arrives at mid from position high is unknown — it might be a 0, 1, or 2. You must examine it again before moving on. Advancing mid here is the single most common bug in Dutch National Flag implementations.


Why is it so fast?

Every element is examined by mid exactly once. After any swap, either mid advances (making progress toward high) or high retreats (shrinking the unsorted zone). The total number of pointer moves is bounded by n. No element is ever touched more than twice (once by mid, at most once by a swap).

n = 1,000,000  →  at most 2,000,000 operations
General sort   →  ~20,000,000 comparisons (n log n)
Two-pass count →  exactly 2n operations (two passes)
DNF            →  ≤ 2n operations (one pass)
ApproachPassesTimeSpace
General sort1O(n log n)O(1) or O(log n)
Count and rebuild2O(n)O(1)
Dutch National Flag1O(n)O(1)

When should I reach for this? (the trigger list)

Reach for three-pointer partition when you see:

  • An array with exactly 3 distinct values (or categories) that need to be grouped.
  • "Sort in-place using one pass" — the signature of DNF.
  • A two-pointer partition problem where one of the "keep" values actually needs to go to two different places (front vs. back vs. middle).
  • Problems about 0s, 1s, 2s; negative, zero, positive; less than, equal to, greater than pivot.
  • Binary partition problems (two-way, using only low and high without mid) — DNF generalises those.

The same trick in four disguises

Disguise 1 — Sort Colors (LC #75)

The exact problem above. Identical code. This is the canonical DNF problem.

Disguise 2 — Separate Negatives, Zeros, Positives

Three categories: negative (0), zero (1), positive (2). Same three pointers, same skeleton, different value labels:

def sort_by_sign(nums):
    low, mid, high = 0, 0, len(nums) - 1
    while mid <= high:
        if nums[mid] < 0:
            nums[low], nums[mid] = nums[mid], nums[low]
            low += 1; mid += 1
        elif nums[mid] > 0:
            nums[mid], nums[high] = nums[high], nums[mid]
            high -= 1
        else:
            mid += 1

Same bones. Only the "what counts as 0/1/2" changes.

Disguise 3 — Move all zeroes and ones to ends (custom problem)

"Given an array of 0s, 1s, and 2s, move 0s to the front and 2s to the back, with 1s in the middle." That is literally the DNF problem stated differently. Use the exact same code.

Level up — Two-way partition (the simpler cousin)

If you only have two categories (e.g., even/odd, negative/non-negative), you only need two pointers — low and high — converging from both ends (opposite-direction two pointers from chapter 1.01), or slow and fast (same-direction from chapter 1.02). DNF is the natural extension to three categories. Understanding both makes it easy to generalise to any fixed number of categories.

Level up — Sort Colors II (K colours, LC #143 variant)

What if there are K distinct values instead of 3? One approach: run K-1 passes of partition (like selection sort but by group). Better: use a hash map to count, then rebuild — O(n) two-pass, O(K) space. DNF's single-pass O(1) trick only works cleanly for exactly 3 groups; for more, we generally accept two passes.


Traps that catch beginners

Watch out for these

  • Advancing mid after a swap with high. The element that arrived from the right is unseen. Advancing past it means you never examine it — some 0s or 2s will land in the wrong section.
  • Using mid < high instead of mid <= high. The element at mid == high is still unsorted — the loop must process it.
  • Forgetting the invariant when the 0-swap arrives a 1. When you swap nums[mid] (a 0) with nums[low], the element at low was a 1 (it was in the proven-1 zone between low and mid). That 1 is now at mid, which is correct — it's in the unsorted→middle zone. So advancing mid is safe.
BugFix
mid++ after swapping with highOnly advance mid for 0 (red) and 1 (white); not for 2 (blue)
while mid < highUse mid <= high — element at the boundary must be examined
General sort instead of DNFIf only 3 distinct values, always ask "can I use Dutch National Flag?"

Say it like a pro (interview one-liner)

"Since there are exactly three distinct values, I'll use the Dutch National Flag algorithm — three pointers maintaining the invariant that everything left of low is a 0, everything between low and mid is a 1, and everything right of high is a 2. Each element is examined exactly once, giving O(n) time and O(1) space in a single pass."


Remember this forever

Dutch National Flag — Three-Way Partition

Three pointers: low (end of 0s), mid (current), high (start of 2s). Examine nums[mid]:

  • 0 (red) → swap with nums[low], low++, mid++
  • 2 (blue) → swap with nums[high], high-- (do not advance mid)
  • 1 (white)mid++

Stop when mid > high. The unsorted middle has vanished.


Cost: O(n) time, O(1) space, one pass · Trigger: 3 distinct values, sort in-place · Key trap: never advance mid after a blue swap


Check yourself

Why do we NOT advance `mid` after swapping with `high`?

Because the element that just arrived at mid (from position high) has never been examined. It could be a 0, 1, or 2. If we advanced mid, we'd skip it entirely — it would end up in the wrong section. We only advance mid when we know the element at mid is correctly placed (either it's a 1, or we just swapped a 0 in from the proven-1 zone).

After a 0-swap, why is it safe to advance `mid`?

When nums[mid] is a 0, we swap it with nums[low]. What was at nums[low]? It was in the zone [low..mid-1], which the invariant guarantees contains only 1s. So after the swap, nums[mid] is now a 1 — already correctly placed in the middle zone. We can safely advance past it.

The loop condition is `while mid <= high`. What happens if you use `mid < high`?

The element at position mid == high never gets processed. If it's a 0 or a 2, it stays in the middle zone — the final array is unsorted at that position. The <= is critical: the boundary element is still "unsorted" and must be examined.

Can you use Dutch National Flag for four distinct values?

Not directly with three pointers. For four values, you'd need four pointers (or two passes of DNF). The single-pass O(1) trick works cleanly only for exactly three groups. For K groups, the general approach is two passes: count, then rebuild — O(n) time, O(K) space.


Practice problems

ProblemDifficultyWhat to noticeLink
Sort ColorsEasyThe canonical DNF problem — exact code from this pageLC #75
Move ZeroesEasyTwo-way DNF: zeros and non-zerosLC #283
Partition Array by Odd/EvenEasyTwo-way: odds to front, evens to backLC #905
Partition Array Around PivotMediumThree-way: less, equal, greater than pivotLC #2161
Sort Array By Parity IIMediumTwo interleaved partitions — think about two pointersLC #922

When you can write the Sort Colors solution from scratch, explain why mid doesn't advance after a blue swap, and state the invariant in one sentence — you have fully learned this pattern.

Next up: Sliding Window — Fixed Size, where instead of pointers on individual elements, we maintain a whole window of K elements as it slides across the array.