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 · What you must know before starting (two pointers from scratch)
- Part A · Counting: two passes
- Part B · Dutch National Flag: one pass with low, mid, high
- Part C · Revision page
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
| kind | how the pointers move | example |
|---|---|---|
| Opposite ends | left 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 direction | both 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 pointers | a loop fixes one element, then opposite-end pointers search the rest | 3Sum |
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:
(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
- 1 ≤ n ≤ 300. Tiny. The built-in sort (O(n log n)) would easily pass, which is exactly why the question bans it. The point is to use the special fact that there are only three values.
- nums[i] is 0, 1 or 2. Only three possible values. This is the key: we never need to compare two numbers in general, we only need to know which of three groups each one belongs to.
- n ≥ 1, so the list is never empty. (Our code still works on an empty list.)
- Follow-up: can you do it in one pass with constant extra space? Part A is two passes; Part B answers the follow-up.
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.
- While count0 is still above 0: write 0 at
nums[i], moveiforward, take one off count0. In the example this fills indexes 0, 1, 2 and count0 goes 3 → 2 → 1 → 0. - When count0 hits 0, all the zeros are placed.
istays where it is (index 3) and the next loop does the same with 1s for count1 rounds, filling 3, 4, 5. - The third loop writes 2s for count2 rounds, filling 6, 7, 8.
→ 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.
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
- Count the 0s, 1s and 2s in one loop.
- Set
i = 0. - Write count0 zeros starting at i, moving i each time.
- Write count1 ones from where i is now.
- Write count2 twos from where i is now.
6Code (Python)
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 -= 17Code line by line
| line | what it means |
|---|---|
| count0 = count1 = count2 = 0 | Three 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 = 0 | The write pointer. It moves forward through the whole list during pass 2. |
| while count0 > 0: nums[i] = 0 i += 1 count0 -= 1 | Write 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.
| step | loop | i (write here) | value written | counter after | list after this step |
|---|---|---|---|---|---|
| 1 | zeros | 0 | 0 | count0 = 2 | [0,1,0,2,0,1,2,0,2] |
| 2 | zeros | 1 | 0 | count0 = 1 | [0,0,0,2,0,1,2,0,2] |
| 3 | zeros | 2 | 0 (was already 0) | count0 = 0 → loop ends | [0,0,0,2,0,1,2,0,2] |
| 4–6 | ones | 3, 4, 5 | 1, 1, 1 | count1 = 0 | [0,0,0,1,1,1,2,0,2] |
| 7–9 | twos | 6, 7, 8 | 2, 2, 2 | count2 = 0 | [0,0,0,1,1,1,2,2,2] |
9Complexity & remember
- Time O(n): pass 1 is n steps. In pass 2, the three while loops together run count0 + count1 + count2 = n times (3 + 3 + 3 = 9 in the example). Total about 2n, which is still O(n). But it is two passes over the list, and the follow-up asks for one.
- Space O(1): three counters and one index.
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
- Only three values → every value goes to one of three groups: the left group (0), the middle group (1) or the right group (2).
- Constant space → we can't use a second list. We must swap values inside nums.
- n ≤ 300 → speed isn't the issue; the challenge is doing it in one pass.
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:
| region | indexes | promise | who owns it |
|---|---|---|---|
| left stripe | 0 … low−1 | all 0s | low (the first spot after the 0s) |
| middle stripe | low … mid−1 | all 1s | mid (the first spot after the 1s) |
| unknown | mid … high | not looked at yet | still to be sorted |
| right stripe | high+1 … n−1 | all 2s | high (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?"
- 0 → hand it to low (it belongs in the left stripe).
- 1 → keep it; it's already in the right place (just after the 1s).
- 2 → hand it to high (it belongs in the right stripe).
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.
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.
→ 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.
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.→ 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
low = 0,mid = 0,high = n − 1.- While
mid <= high(the unknown region isn't empty): - If nums[mid] is 0: swap nums[low] and nums[mid], then
low += 1andmid += 1. - If nums[mid] is 1:
mid += 1. - If nums[mid] is 2: swap nums[mid] and nums[high], then
high -= 1only. Don't move mid.
6Code (Python)
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 uncheckedThe 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
| line | what it means |
|---|---|
| low, mid, high = 0, 0, len(nums) - 1 | All 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 += 1 | Zeros region grows by one. The value now at mid is a known 1, so the ones region grows too. |
| elif nums[mid] == 1: mid += 1 | A 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 -= 1 | The 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.
| step | low | mid | high | nums[mid] | decision (which pointer moves, why) | list after this step |
|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 8 | 1 | a 1 stays → mid +1 | [1,1,0,2,0,1,2,0,2] |
| 2 | 0 | 1 | 8 | 1 | a 1 stays → mid +1 | [1,1,0,2,0,1,2,0,2] |
| 3 | 0 | 2 | 8 | 0 | swap idx 0 ↔ 2 → low +1, mid +1 | [0,1,1,2,0,1,2,0,2] |
| 4 | 1 | 3 | 8 | 2 | swap idx 3 ↔ 8 (2 with 2) → high −1, mid stays | [0,1,1,2,0,1,2,0,2] |
| 5 | 1 | 3 | 7 | 2 | still a 2! swap idx 3 ↔ 7 → high −1, mid stays | [0,1,1,0,0,1,2,2,2] |
| 6 | 1 | 3 | 6 | 0 | the value that came back is 0 → swap idx 1 ↔ 3 → low +1, mid +1 | [0,0,1,1,0,1,2,2,2] |
| 7 | 2 | 4 | 6 | 0 | swap idx 2 ↔ 4 → low +1, mid +1 | [0,0,0,1,1,1,2,2,2] |
| 8 | 3 | 5 | 6 | 1 | a 1 stays → mid +1 | [0,0,0,1,1,1,2,2,2] |
| 9 | 3 | 6 | 6 | 2 | mid = high: swap with itself → high −1 = 5 | [0,0,0,1,1,1,2,2,2] |
| end | 3 | 6 | 5 | – | 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.
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
- Time O(n), one pass. Each loop round does O(1) work (one comparison, maybe one swap) and shrinks the unknown region by exactly one: either mid moves right or high moves left. The teacher puts it as: if mid travels x steps, high travels the other n − x, so the loop runs x + (n − x) = n times in total.
- Space O(1): three index variables.
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) | |
|---|---|---|
| idea | count each colour, then repaint the list | three pointers keep four regions; mid sends each value to its stripe |
| passes | 2 (count, then write) | 1 |
| time / space | O(2n) = O(n) / O(1) | O(n) / O(1) |
| works for more colours? | yes, with k counters | only for 3 groups |
| mid sees | swap with | low | mid | high | why |
|---|---|---|---|---|---|
| 0 | low | +1 | +1 | – | the value coming from low is a known 1 |
| 1 | nothing | – | +1 | – | 1 is already at the end of the ones |
| 2 | high | – | stay | −1 | the value coming from high is unchecked |
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.
✗
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
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