Coding Interview Patterns Module 4 – Two pointers
Dutch national flag: three-way partitioning
Group a list into values below, equal to and above a pivot in one pass with O(1) memory: the problem, its examples and what the obvious answers cost.
What you will learn
- Partition an array into three groups in one pass with three pointers
- Prove that each swap keeps the four regions of the invariant intact
- Explain every pointer move of the one-pass partition from its invariant
- Use the three-way partition inside quicksort and quickselect
- Measure how equal keys slow down a two-way quicksort
Before you start
What you’ll learn · Before you start
What you will learn
- Partition an array into three groups in one pass with three pointers
- Prove that each swap keeps the four regions of the invariant intact
- Explain every pointer move of the one-pass partition from its invariant
- Use the three-way partition inside quicksort and quickselect
- Measure how equal keys slow down a two-way quicksort
Before you start
On this page
The Dutch national flag problem asks you to rearrange a list into three groups, the values below a pivot: The value a partition compares every item with: items below it go to one side and items above it to the other. Quicksort takes its pivot from the part of the list it is partitioning., the values equal to it and the values above it, in one pass with O(1) extra memory: for 1,000,000 parcel levels, one look at each. Dijkstra used it as an exercise in deriving and proving programs, with items in the three colours of the Dutch flag.
The standard answer keeps three indices and moves each value into its group with at most one swap, so the pass takes n steps. The same step, a three-way partition: Rearranging a list around a pivot value into three groups: the values below it, the values equal to it and the values above it, with the order inside each group left free., keeps quicksort fast when many keys are equal.
The problem: three groups around a pivot, in one pass
The problem
A sorting hub scans parcels into a list, and levels holds each parcel’s priority level as an integer. Given a
level p, rearrange levels in place so that every level below p comes first, then every level equal to p,
then every level above it, and return the pair (i, j) with levels[:i] below p, levels[i:j] equal to it and
levels[j:] above it. The order inside a group does not matter. Use one pass and O(1) extra memory. Limits:
1 ≤ n ≤ 1,000,000, and the levels and p are 32-bit integers.
levels = [2, 0, 2, 1, 1, 0]withp = 1becomes[0, 0, 1, 1, 2, 2]and gives(2, 4).levels = [5, 9, 5, 1, 7]withp = 5gives(1, 3): 1 first, the two 5s next, then 9 and 7 in either order.levels = [6, 8, 7]withp = 4gives(0, 0): the pivot need not be in the list, and here every level is above it.
The first example, before and after:
What the call must do to the first example, with p = 1
Text description of the diagram
The figure has two rows of six slots.
- Before the call, the levels are 2, 0, 2, 1, 1 and 0, and the pivot p is 1.
- After the call, the same levels form three groups: 0 and 0 below p in slots 0 and 1, 1 and 1 equal to p in slots 2 and 3, and 2 and 2 above p in slots 4 and 5. A line before slot 2 marks i = 2 and a line before slot 4 marks j = 4, so the call returns (2, 4): levels[:i] is below p, levels[i:j] is equal to p and levels[j:] is above p.
Checkpoint
Check: Three groups around a pivot, in one pass
Sorting or two passes: correct, but more work than one pass
Two correct answers come first:
- Sort the list. Sorted values are grouped, so
(i, j)follows by counting. But a sort also orders the values inside each group, which nobody asked for, and pays for it with O(n log n) comparisons instead of one look per value. - Two reader–writer passes. First swap every level below
pto the front, the way the writer of the previous lesson keeps items, then swap every level equal topright behind them. That is O(n) time and O(1) memory, but it reads part of the list twice.
"""Two correct answers that do not meet the brief: sorting, and two reader-writer passes.
Each counts its work: comparisons for the sort; items read and swaps for the two passes."""
import functools
import random
def by_sorting(levels, p):
compares = 0
def compare(x, y):
nonlocal compares
compares += 1
return (x > y) - (x < y)
levels.sort(key=functools.cmp_to_key(compare))
i = sum(1 for v in levels if v < p)
j = i + sum(1 for v in levels if v == p)
return (i, j), compares
def by_two_passes(levels, p):
reads = swaps = 0
w = 0
for r in range(len(levels)): # pass 1: every level below p to the front
reads += 1
if levels[r] < p:
levels[w], levels[r] = levels[r], levels[w]
swaps += 1
w += 1
i = w
for r in range(i, len(levels)): # pass 2: every level equal to p right after them
reads += 1
if levels[r] == p:
levels[w], levels[r] = levels[r], levels[w]
swaps += 1
w += 1
return (i, w), reads, swaps
for levels, p in [([2, 0, 2, 1, 1, 0], 1), ([5, 9, 5, 1, 7], 5), ([6, 8, 7], 4)]:
a, b = list(levels), list(levels)
(i, j), _ = by_sorting(a, p)
two, _, _ = by_two_passes(b, p)
print(f"p = {p}: {levels} -> sorted {a}, ({i}, {j}); two passes {b}, {two}")
rng = random.Random(7)
n = 100_000
levels = [rng.randint(0, 9) for _ in range(n)]
_, compares = by_sorting(list(levels), 5)
_, reads, swaps = by_two_passes(list(levels), 5)
print()
print(f"{n:,} random levels from 0 to 9, p = 5:")
print(f" sorting: {compares:,} comparisons")
print(f" two passes: {reads:,} items read, {swaps:,} swaps")
print(f" one pass reads each level once: {n:,} items read") Output
p = 1: [2, 0, 2, 1, 1, 0] -> sorted [0, 0, 1, 1, 2, 2], (2, 4); two passes [0, 0, 1, 1, 2, 2], (2, 4) p = 5: [5, 9, 5, 1, 7] -> sorted [1, 5, 5, 7, 9], (1, 3); two passes [1, 5, 5, 9, 7], (1, 3) p = 4: [6, 8, 7] -> sorted [6, 7, 8], (0, 0); two passes [6, 8, 7], (0, 0) 100,000 random levels from 0 to 9, p = 5: sorting: 714,264 comparisons two passes: 150,034 items read, 60,036 swaps one pass reads each level once: 100,000 items read
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sort_or_two_passes.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
On 100,000 random levels the sort made about seven comparisons per level and the two passes read one and a half levels per level, where a single pass reads each level once. Neither meets the brief, and the gap grows where the step repeats, as in quicksort. Step through the two passes:
Program · trace/two_passes_steps.py
levels = [2, 0, 2, 1, 1, 0]
p = 1
w = 0
for r in range(len(levels)):
if levels[r] < p:
levels[w], levels[r] = levels[r], levels[w]
w += 1
i = w
for r in range(i, len(levels)):
if levels[r] == p:
levels[w], levels[r] = levels[r], levels[w]
w += 1
print(levels, (i, w)) Variables
- just taken by the loop
- changed in this step
- passed by the loop
- read next
- where the loop stopped
- the stop value, never given
Printed so far
Nothing printed yet[0, 0, 1, 1, 2, 2] (2, 4)
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 36 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
levels = [2, 0, 2, 1, 1, 0]makeslevelsa list of 6 items. - Line 2.
p = 1setspto1. - Line 3.
w = 0setswto0. - Line 4.
for r in range(len(levels)):gives its first number:ris0. - Line 5.
levels[r] < pis false (pis1,levelsis[2, 0, 2, 1, 1, 0]andris0), so Python skips the block underif levels[r] < p:. - Line 4.
for r in range(len(levels)):gives the next number:ris1. - Line 5.
levels[r] < pis true (pis1,levelsis[2, 0, 2, 1, 1, 0]andris1), so the block underif levels[r] < p:runs. - Line 6.
levels[w], levels[r] = levels[r], levels[w]setslevelsto[0, 2, 2, 1, 1, 0](it was[2, 0, 2, 1, 1, 0]). - Line 7.
w += 1changeswfrom0to1. - Line 4.
for r in range(len(levels)):gives the next number:ris2. - Line 5.
levels[r] < pis false (pis1,levelsis[0, 2, 2, 1, 1, 0]andris2), so Python skips the block underif levels[r] < p:. - Line 4.
for r in range(len(levels)):gives the next number:ris3. - Line 5.
levels[r] < pis false (pis1,levelsis[0, 2, 2, 1, 1, 0]andris3), so Python skips the block underif levels[r] < p:. - Line 4.
for r in range(len(levels)):gives the next number:ris4. - Line 5.
levels[r] < pis false (pis1,levelsis[0, 2, 2, 1, 1, 0]andris4), so Python skips the block underif levels[r] < p:. - Line 4.
for r in range(len(levels)):gives the next number:ris5. - Line 5.
levels[r] < pis true (pis1,levelsis[0, 2, 2, 1, 1, 0]andris5), so the block underif levels[r] < p:runs. - Line 6.
levels[w], levels[r] = levels[r], levels[w]setslevelsto[0, 0, 2, 1, 1, 2](it was[0, 2, 2, 1, 1, 0]). - Line 7.
w += 1changeswfrom1to2. - Line 4.
for r in range(len(levels)):has no number left: a range stops before its stop value,6, so the loop ends. - Line 8.
i = wsetsito2. Key point The first pass has read all six levels and moved the two 0s to the front. The second pass starts at position 2 and reads the four levels after them again. - Line 9.
for r in range(i, len(levels)):gives its first number:ris2. - Line 10.
levels[r] == pis false (pis1,levelsis[0, 0, 2, 1, 1, 2]andris2), so Python skips the block underif levels[r] == p:. - Line 9.
for r in range(i, len(levels)):gives the next number:ris3. - Line 10.
levels[r] == pis true (pis1,levelsis[0, 0, 2, 1, 1, 2]andris3), so the block underif levels[r] == p:runs. - Line 11.
levels[w], levels[r] = levels[r], levels[w]setslevelsto[0, 0, 1, 2, 1, 2](it was[0, 0, 2, 1, 1, 2]). - Line 12.
w += 1changeswfrom2to3. - Line 9.
for r in range(i, len(levels)):gives the next number:ris4. - Line 10.
levels[r] == pis true (pis1,levelsis[0, 0, 1, 2, 1, 2]andris4), so the block underif levels[r] == p:runs. - Line 11.
levels[w], levels[r] = levels[r], levels[w]setslevelsto[0, 0, 1, 1, 2, 2](it was[0, 0, 1, 2, 1, 2]). - Line 12.
w += 1changeswfrom3to4. - Line 9.
for r in range(i, len(levels)):gives the next number:ris5. - Line 10.
levels[r] == pis false (pis1,levelsis[0, 0, 1, 1, 2, 2]andris5), so Python skips the block underif levels[r] == p:. - Line 9.
for r in range(i, len(levels)):has no number left: a range stops before its stop value,6, so the loop ends. - Line 13.
print(levels, (i, w))prints[0, 0, 1, 1, 2, 2] (2, 4). That was the last line, so the program ends. Ten reads for six levels. The two passes give the right groups and (2, 4), but a single pass would read each level once.
Checkpoint
Check: Sorting or two passes, and what they cost
What to notice: four regions, one swap per value
Picture the list as four regions with fixed meanings: values below p, values equal to p, values not examined yet,
and values above p. At the start the unexamined region is the whole list; at the end it is empty, and the other
three regions are the answer. If every step examines one value and moves it into its region with at most one swap,
the unexamined region shrinks by one value per step, and the pass ends after n steps.
The four regions of the list while the pass runs
Text description of the diagram
The diagram shows the list split into four regions, from left to right: the values below p (marked < p), the values equal to p (marked = p), the values not examined yet (marked not yet), and the values above p (marked > p). At the start the region of values not examined yet is the whole list, and when it is empty the other three regions are the answer.
Checkpoint
Check: Four regions, one swap per value
These questions start with your weakest idea.
Practice that adapts to you
Practise this lesson
9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Three pointers in one pass, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- A dry run on the first example, one row per step
- The invariant, and a proof that each move keeps it
- The code in 14 languages
- Moving mid after the far swap: the bug, found by trying every small list
- Edge cases and slips to check
- Equal keys in quicksort, counted
- Quickselect and other variants
- Practise: partition around a value
- Partition a list around a value in one pass
- A practice set with sample answers
- Interview questions
- In the one-pass three-way partition, each swap sends a value to one end of the list. After which swaps may mid move on, and after which must it stay? Derive both rules from the invariant, and find a small input that breaks a version with the wrong rule.
- Your hand-written quicksort sorts a million exam marks from 0 to 100 and is far slower than on a million distinct values. Explain why, estimate the comparisons, and fix it. What does your fix cost on distinct values?
- You must group a million order records into overdue, due today and later, in place, keeping the original order inside each group. Can the one-pass swap partition do it? What are your options, and what does each cost?
- Key takeaways
3 diagrams · 7 runnable examples (Python) · 1 exercise with sample tests · 3 interview answers · about 44 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- pivot
- The value a partition compares every item with: items below it go to one side and items above it to the other. Quicksort takes its pivot from the part of the list it is partitioning.
- three-way partition
- Rearranging a list around a pivot value into three groups: the values below it, the values equal to it and the values above it, with the order inside each group left free.
References
- Dutch national flag (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology)
- Quicksort with Equal Keys (SIAM Journal on Computing 6(2), 1977) (Society for Industrial and Applied Mathematics (R. Sedgewick))
- Engineering a sort function (Software: Practice and Experience 23(11), 1993) (Wiley (J. L. Bentley and M. D. McIlroy))
- std::swap (cppreference.com)
- std::stable_partition (cppreference.com)
- Destructuring assignment: swapping variables (JavaScript reference) (MDN Web Docs)
- Assignment statements (The Python Language Reference) (Python Software Foundation)
- The Go Programming Language Specification: assignment statements (The Go Authors)
- The Go Programming Language Specification: switch statements (The Go Authors)
- Collections.swap(List, int, int) (Java SE 25 API) (Oracle)
- slice::swap (The Rust Standard Library) (The Rust Project)
- Tuple types: tuple assignment and deconstruction (C# reference) (Microsoft)
- swapAt(_:_:) (Swift standard library) (Apple)
- Idioms: swap two variables (Kotlin documentation) (JetBrains)
- Arrays (PHP manual): array assignment copies the value (The PHP Group)
- Metrics/MethodLength (RuboCop documentation) (RuboCop)
- Records (Dart language documentation) (Dart (Google))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress