DSA sheet · Arrays · Two Pointers pattern

Sort Colors (Dutch National Flag)

This is the third two-pointer question in the sheet. The array holds only three kinds of values, 0, 1 and 2, and we must sort it without the built-in sort. The teacher first solves it the easy way: count how many 0s, 1s and 2s there are, then write them back. That takes two passes. Then she answers the follow-up ("one pass, constant extra space") with the Dutch National Flag algorithm, which uses three pointers: low, mid and high.

Why it matters: the Dutch National Flag idea is "split the array into regions and keep each region's promise at every step". The same thinking appears in quicksort's three-way partition and in many "group these values in place" questions.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a pointer here?

In array questions, a pointer is just a variable that holds an index (a position) in the array. It isn't a memory address like in C. If low = 2, then "low points to index 2", and nums[low] is the value sitting there.

The brute force over all pairs, and why we can often skip pairs

Many array questions look at pairs of positions (i, j). Two nested loops can try every pair: about n·(n−1)/2 of them, which is O(n²). The two-pointer idea is that the shape of the array (it's sorted, or the values are limited, or we only care about the ends) lets us throw away many pairs without checking them. Then each pointer moves only forward (or only backward), and the total work is O(n).

The kinds of two-pointer movement

kindhow the pointers moveexample
Opposite endsleft starts at index 0, right at the last index. They walk towards each other. At every step we move one of them, the one that can't be part of a better answer.pair sum in a sorted array, Container With Most Water, palindromes
Same directionboth start at the left. A fast (read) pointer looks at every element; a slow (write) pointer marks where the next "good" element should go.Move Zeroes, remove duplicates
Three pointers (this page)low and mid start at the left, high at the right. mid reads; low and high are the borders of the 0-region and the 2-region.Sort Colors / Dutch National Flag
Fix one + two pointersa loop fixes one element, then opposite-end pointers search the rest3Sum

Why two pointers here and not the other array patterns?

The teacher's sheet has four array patterns: two pointers, sliding window, prefix sum and Kadane's algorithm. The last three are all about a contiguous subarray (a piece of the array with no gaps): its sum, its product, its best value. Sort Colors has no window or subarray at all. We only look at particular positions and swap values between them. So she picks two pointers straight away.

What does "in place" mean?

In place means we change the given list itself. We don't build a new list and return it. LeetCode's sortColors returns nothing (None); it checks the same list afterwards. Swapping two positions, nums[a], nums[b] = nums[b], nums[a], is the main in-place tool.


Part A · Counting: two passes

LeetCode 75

1The question in simple words

You get a list nums of n objects. Each one is coloured red, white or blue, written as numbers: 0 = red, 1 = white, 2 = blue. Rearrange the list in place so that all 0s come first, then all 1s, then all 2s. You are not allowed to call the library sort.

The teacher's example has nine values, three of each colour:

index012345678
input110201202
output000111222same list, now in order

(The video's dry run starts from this list. LeetCode's own example is [2,0,2,1,1,0] → [0,0,1,1,2,2].)

2What the constraints tell us

3Intuition: count, then repaint

How would you sort this by hand? You'd count: "three 0s, three 1s, three 2s". Then you'd write three 0s, then three 1s, then three 2s over the old list. The order of the original values doesn't matter at all, because every 0 looks the same as every other 0.

So the plan is two passes: pass 1 counts, pass 2 overwrites.

4Building the logic from the example

Pass 1: three counters

Keep count0, count1, count2, all starting at 0. Walk through the list once. If the value is 0, add one to count0; if it's 1, add one to count1; otherwise (it must be 2) add one to count2. For the example we get 3, 3, 3.

Pass 2: one write pointer, three while loops

Now keep a pointer i that starts at index 0. It marks the next position to overwrite, and it travels through the whole list across all three loops.

Doubt 1: index 2 already held a 0 when we wrote 0 there. Should we check first and skip it?
→ You can add that check, but there's no need. The loop runs exactly count0 times either way, and writing a 0 over a 0 changes nothing. The teacher mentions this and leaves the write unconditional.
Doubt 2: why doesn't i reset between the three loops?
→ Because the 1s must start right after the last 0, and the 2s right after the last 1. i is the "next free spot" for the whole list. Resetting it would overwrite the zeros we just wrote.

5Approach steps

  1. Count the 0s, 1s and 2s in one loop.
  2. Set i = 0.
  3. Write count0 zeros starting at i, moving i each time.
  4. Write count1 ones from where i is now.
  5. Write count2 twos from where i is now.

6Code (Python)

Counting: two passes, O(n) time, O(1) space
class Solution:
    def sortColors(self, nums):
        count0 = count1 = count2 = 0
        for x in nums:                 # pass 1: count each colour
            if x == 0:
                count0 += 1
            elif x == 1:
                count1 += 1
            else:
                count2 += 1

        i = 0                          # next position to overwrite
        while count0 > 0:              # pass 2: write the 0s
            nums[i] = 0
            i += 1
            count0 -= 1
        while count1 > 0:              # then the 1s
            nums[i] = 1
            i += 1
            count1 -= 1
        while count2 > 0:              # then the 2s
            nums[i] = 2
            i += 1
            count2 -= 1

7Code line by line

linewhat it means
count0 = count1 = count2 = 0Three tallies, one per colour. Only three numbers, so this is constant extra space.
for x in nums: ...Pass 1. Look at each value once and add it to the right tally. The else means "it's a 2", since only three values exist.
i = 0The write pointer. It moves forward through the whole list during pass 2.
while count0 > 0: nums[i] = 0 i += 1 count0 -= 1Write one 0, step to the next spot, and note that one fewer 0 is left to place. Stops after exactly count0 writes.
while count1 > 0: ...Same idea for the 1s, continuing from where the zeros ended.
while count2 > 0: ...Same for the 2s. After this, i equals n: every position was written once.

8Dry run (hand table)

nums = [1, 1, 0, 2, 0, 1, 2, 0, 2]. After pass 1: count0 = 3, count1 = 3, count2 = 3.

steploopi (write here)value writtencounter afterlist after this step
1zeros00count0 = 2[0,1,0,2,0,1,2,0,2]
2zeros10count0 = 1[0,0,0,2,0,1,2,0,2]
3zeros20 (was already 0)count0 = 0 → loop ends[0,0,0,2,0,1,2,0,2]
4–6ones3, 4, 51, 1, 1count1 = 0[0,0,0,1,1,1,2,0,2]
7–9twos6, 7, 82, 2, 2count2 = 0[0,0,0,1,1,1,2,2,2]
after 0s000201202grey = done; i is at index 3
i
after 1s000111202i is at index 6
i

9Complexity & remember

Remember countingCount 0s, 1s, 2s. Then one pointer i writes count0 zeros, count1 ones, count2 twos. Correct and fast, but two passes.

Part B · Dutch National Flag: one pass with low, mid, high

LeetCode 75 · follow-up

1The question again, with the new goal

Same question, but now: one pass over the list and constant extra space. No counting first. Each value must be put in the right place as we meet it.

The name comes from the Dutch flag, which has three horizontal stripes. We're building three "stripes": 0s, 1s, 2s.

2What the constraints tell us

3Intuition: three pointers hold a meeting

The teacher tells it as a story. Three pointers, low, mid and high, sit in a meeting and divide the work. low and mid start at index 0; high starts at the last index, n − 1. Each one makes a promise:

regionindexespromisewho owns it
left stripe0 … low−1all 0slow (the first spot after the 0s)
middle stripelow … mid−1all 1smid (the first spot after the 1s)
unknownmid … highnot looked at yetstill to be sorted
right stripehigh+1 … n−1all 2shigh (the last spot before the 2s)
  index:   0 .. low-1 | low .. mid-1 | mid .. high | high+1 .. n-1
  holds:     0 0 0    |    1 1 1     |  ? ? ? ?    |    2 2 2
             zeros    |    ones      |  UNKNOWN    |    twos

At the start, low = mid = 0 and high = n − 1, so the zeros, ones and twos regions are all empty and the whole list is "unknown". That's true, so every promise holds from the beginning.

mid is the one doing the work. It looks at the first unknown value and asks "what are you?"

Each step shrinks the unknown region by one. When the unknown region is empty, the list is sorted.

4Building the logic from the example

Where do the pointers end up when it's sorted?

The teacher first draws the finished list [0,0,0,1,1,1,2,2,2] and asks where each pointer must be for its promise to be true. "0..low−1 is all zeros" → low points at index 3 (the first 1). "low..mid−1 is all ones" → mid points just past the last 1, index 6. "high+1..n−1 is all twos" → high points at index 5, just before the first 2. So at the end, mid = high + 1: mid has walked from the left, high has walked from the right, and they have crossed.

The loop condition: while mid <= high

The unknown region is mid … high. It still has a value in it as long as mid ≤ high. When mid = high there's one unknown value left, and it must still be checked. So the loop runs while mid <= high and stops the moment mid passes high.

Doubt 1: why not while mid < high?
→ That stops while one value (at index mid = high) is still unchecked. Try [1, 0]: mid = 0 sees 1 → mid = 1. Now mid = high = 1, and with < the loop stops. The 0 at index 1 is never sent left, so the result [1, 0] is wrong. With <=, mid checks it, swaps it with low → [0, 1] ✓. The teacher first says "less than" and then corrects herself to "less than or equal" for this reason: the pointers only truly cross after they've been equal.

Case 1: nums[mid] is 1 → just move mid

In the example, mid starts at index 0 and sees a 1. A 1 belongs in the middle stripe, which ends at mid − 1. If mid moves forward by one, this 1 becomes the last cell of the middle stripe. No swap needed: mid += 1. Index 1 is also 1, so mid moves again.

Case 2: nums[mid] is 0 → swap with low, move both

Now mid is at index 2 and sees a 0. It belongs at the end of the left stripe, which is exactly index low. So swap nums[low] and nums[mid]: [1,1,0,…] becomes [0,1,1,…].

Now the 0 is at index low, so the zeros region has grown by one: low += 1 keeps "0..low−1 are zeros" true. The value that came to mid is a 1 (it used to sit at the start of the 1s), so the ones region also grows: mid += 1. Both move.

Doubt 2: after swapping with low, why is it safe to move mid without looking at the new value?
→ Because we know what came from low. Index low is either inside the 1s region (if low < mid), so it held a 1, or low equals mid (no 1s yet), so we just swapped the 0 with itself. Either way, the value now at mid is already settled. Every value at or left of mid has already been checked by mid earlier. Nothing new came in from the unknown part.

Case 3: nums[mid] is 2 → swap with high, move only high

mid is at index 3 and sees a 2. It belongs in the right stripe, whose last free spot is index high. Swap nums[mid] and nums[high], then high -= 1 so that "high+1..n−1 are twos" stays true.

But mid does NOT move. Look at what happens in the example: index 8 (high) held a 2. After the swap, mid still sees a 2! It has to send that one right as well: swap with the new high (index 7), which holds a 0. Now mid sees a 0, which must still go to the left. If mid had moved forward after the first swap, that 2, and then that 0, would have been left in the wrong place.

Why mid stays put after swapping with highThe value that comes back from high comes out of the unknown region. Nobody has looked at it yet. It could be 0, 1 or 2. So mid must check it again on the next loop round. Compare with Case 2: the value from low came out of the already-checked region, so we knew it was 1.
Doubt 3: the 2 at mid swapped with a 2 at high. Isn't that a wasted swap?
→ It looks wasted, but high still moves left, so the 2s region grows by one and the unknown region shrinks by one. Progress is made. We only care about what mid is holding, not what high held before.

The last value: mid and high on the same index

Near the end of the example, mid and high meet at index 6, which holds a 2. Swapping it with itself changes nothing, then high -= 1 makes high = 5 < mid = 6. They have crossed, the loop stops, and the list is sorted.

5Approach steps

  1. low = 0, mid = 0, high = n − 1.
  2. While mid <= high (the unknown region isn't empty):
  3. If nums[mid] is 0: swap nums[low] and nums[mid], then low += 1 and mid += 1.
  4. If nums[mid] is 1: mid += 1.
  5. If nums[mid] is 2: swap nums[mid] and nums[high], then high -= 1 only. Don't move mid.

6Code (Python)

Dutch National Flag: one pass, O(n) time, O(1) space
class Solution:
    def sortColors(self, nums):
        low, mid, high = 0, 0, len(nums) - 1
        while mid <= high:                     # unknown region mid..high not empty
            if nums[mid] == 0:                 # belongs in the left stripe
                nums[low], nums[mid] = nums[mid], nums[low]
                low += 1
                mid += 1
            elif nums[mid] == 1:               # already in the middle stripe
                mid += 1
            else:                              # a 2: belongs in the right stripe
                nums[mid], nums[high] = nums[high], nums[mid]
                high -= 1                      # mid stays: the new value is unchecked

The teacher writes the swaps as "put nums[low] at mid, then put a 0 at low" (and "put nums[high] at mid, then a 2 at high"), since we already know the value at mid. A plain Python swap does the same thing.

7Code line by line

linewhat it means
low, mid, high = 0, 0, len(nums) - 1All three stripes start empty; the whole list is unknown.
while mid <= high:There is still at least one unchecked value (from mid to high). Stop when mid crosses high.
if nums[mid] == 0:mid found a 0. It belongs at index low, the end of the zeros.
nums[low], nums[mid] = nums[mid], nums[low]Send the 0 to low; the 1 that was at low comes to mid.
low += 1 mid += 1Zeros region grows by one. The value now at mid is a known 1, so the ones region grows too.
elif nums[mid] == 1: mid += 1A 1 is already where it belongs. Just widen the ones region.
else: nums[mid], nums[high] = nums[high], nums[mid]mid found a 2. Send it to high, the last free spot before the 2s. An unchecked value comes back to mid.
high -= 1The twos region grows by one. mid does not move, so the next round checks the value that just arrived.

8Dry run

nums = [1, 1, 0, 2, 0, 1, 2, 0, 2], n = 9. Start: low = 0, mid = 0, high = 8.

steplowmidhighnums[mid]decision (which pointer moves, why)list after this step
10081a 1 stays → mid +1[1,1,0,2,0,1,2,0,2]
20181a 1 stays → mid +1[1,1,0,2,0,1,2,0,2]
30280swap idx 0 ↔ 2 → low +1, mid +1[0,1,1,2,0,1,2,0,2]
41382swap idx 3 ↔ 8 (2 with 2) → high −1, mid stays[0,1,1,2,0,1,2,0,2]
51372still a 2! swap idx 3 ↔ 7 → high −1, mid stays[0,1,1,0,0,1,2,2,2]
61360the value that came back is 0 → swap idx 1 ↔ 3 → low +1, mid +1[0,0,1,1,0,1,2,2,2]
72460swap idx 2 ↔ 4 → low +1, mid +1[0,0,0,1,1,1,2,2,2]
83561a 1 stays → mid +1[0,0,0,1,1,1,2,2,2]
93662mid = high: swap with itself → high −1 = 5[0,0,0,1,1,1,2,2,2]
end365–mid > high → loop stops[0,0,0,1,1,1,2,2,2] ✓

The list at three key moments. Yellow = where the pointers are, grey = a finished stripe.

step 3110201202mid sees 0 → swap with low
LMH
step 5011201202after one 2 went right, mid still sees a 2 → swap with high again
LMH
step 9000111222mid = high = 6, the last unknown value
LM H

Check the promises after step 6 (now low = 2, mid = 4, high = 6): indexes 0..1 = [0, 0] zeros ✓, 2..3 = [1, 1] ones ✓, 4..6 still unknown, 7..8 = [2, 2] twos ✓. Every step keeps all four regions honest.

9Complexity & remember

Remember Dutch National Flag Regions: [0, low) zeros · [low, mid) ones · [mid, high] unknown · (high, n) twos.
0 → swap with low, low++ and mid++. 1 → mid++. 2 → swap with high, high-- only (what came back is unchecked).
Loop while mid <= high.

Part C · Revision page

Counting (Part A)Dutch National Flag (Part B)
ideacount each colour, then repaint the listthree pointers keep four regions; mid sends each value to its stripe
passes2 (count, then write)1
time / spaceO(2n) = O(n) / O(1)O(n) / O(1)
works for more colours?yes, with k countersonly for 3 groups
mid seesswap withlowmidhighwhy
0low+1+1–the value coming from low is a known 1
1nothing–+1–1 is already at the end of the ones
2high–stay−1the value coming from high is unchecked
If you remember only 5 lines 1. Only three values, no library sort; counting works but takes two passes.
2. low = mid = 0, high = n − 1. Unknown region = mid..high.
3. 0 → swap with low, move low and mid.
4. 1 → move mid. 2 → swap with high, move high only.
5. Loop while mid ≤ high; each round shrinks the unknown region by one, so it's one pass.
Mistakes to avoid ✗ moving mid after swapping with high (the new value is never checked)
✗ while mid < high (the last value can be missed, e.g. [1, 0])
✗ comparing with low or high instead of mid (only mid decides)
✗ returning a new list instead of changing nums in place
✗ resetting the write pointer between the three while loops in the counting version
test it yourself (paste under either solution above)
s = Solution()
for nums in ([1, 1, 0, 2, 0, 1, 2, 0, 2], [2, 0, 2, 1, 1, 0], [2, 0, 1], [1, 0], [0], [2, 2, 2]):
    s.sortColors(nums)          # changes nums in place, returns None
    print(nums)
# [0, 0, 0, 1, 1, 1, 2, 2, 2]
# [0, 0, 1, 1, 2, 2]
# [0, 1, 2]
# [0, 1]
# [0]
# [2, 2, 2]

Based on this video: Sort Colors | Dutch National Flag Algorithm