two pointers · study console
Arrays · Strings · Two Pointers

Recognize the shape
before you write the loop.

A rebuild from first principles, tuned for pattern-recognition speed under interview pressure. Not what an array is — how the shape of a two-pointer problem announces itself, and how to state the invariant out loud before the first keystroke.

tier-1 OA + onsite calibration 3 interactive widgets 10 problems · easy → hard
§ 01 — fundamentals that actually matter

Four ideas carry the whole topic

None of them are syntax. You already know how a for-loop works; the depth budget goes entirely on why a technique works and how to spot it under disguise.

In-place mutationThe phrase “O(1) extra space” is a tell. You’re going to track a boundary index and overwrite the array as you go, not build a new one. Every “remove / move / partition in place” problem is this.
Index arithmetic as the unit of workTwo pointers is a discipline for choosing which indices to look at next so each is visited a bounded number of times — collapsing “look at all pairs” into “look at the right pairs.”
Prefix sumsPrecompute prefix[i] = a[0]+…+a[i-1]; then any range sum [l,r] is prefix[r+1] − prefix[l] in O(1). The antidote to a nested loop that recomputes the same running sum. Two-pointer-adjacent: same instinct, different mechanism.
The O(n²)-in-a-nested-loop smell testWhen you write for i: for j>i: over the same array, stop and ask the question that derives every two-pointer solution.
the diagnostic question

What information is the brute force throwing away and recomputing on each pass? If the answer is “ordering it already knows” or “a sum it already had,” the inner loop usually collapses into a moving pointer.

§ 02 — two pointers, derived

Start from the brute force

Canonical problem: given a sorted array, is there a pair summing to a target? The brute force checks every pair and ignores the one fact handed to us for free — that the array is sorted.

brute force · O(n²)
for i in 0..n:
    for j in i+1..n:
        if a[i] + a[j] == target: return (i, j)

Here’s the waste: when a sum comes out too small, the brute force never eliminates anything — it re-derives “too small / too big” from scratch every time. Put one pointer at each end instead, and sortedness lets you retire a whole row or column of the pair-matrix per step:

If a[lo]+a[hi] < target: the sum is too small, and a[hi] is the largest partner available — so a[lo] with anything else is even smaller. Eliminate a[lo], lo++.
If a[lo]+a[hi] > target: symmetric. a[hi] is doomed. hi--.
If equal: done. Each step retires one element permanently — O(n).
invariant — the centerpiece, not a footnote

At every step, no pair that could sum to the target lies outside the window [lo, hi]. Everything stepped past has been proven unable to participate.

Say that sentence out loud in the interview and you pass. Pattern-match “pointers at both ends, move toward the middle” without it and you fall apart the moment the problem is disguised — Container With Most Water, Trapping Rain Water — because you won’t know which pointer to move or why it’s safe.

Why moving the smaller side is always safe

Abstract arguments don’t survive pressure, so make it numeric. Array [1, 4, 6, 8, 11, 15], target 19:

lo=0 (1), hi=5 (15)1+15 = 16 < 19. The biggest partner available to 1 is 15, and even that only reached 16. Every other partner is smaller, so every sum with 1 is below target. Drop it — we lose nothing. lo++.
lo=1 (4), hi=5 (15)4+15 = 19 ✓.

We never checked (1,11), (1,8), (4,11) — but we proved none of the 1-pairs could win before discarding, and the answer survived. Proof-before-discard is the whole game.

pattern-recognition triggers · reach for two pointers when you see
  • Sorted or sortable input + asking for a pair or triplet hitting a target.
  • “Remove / dedupe / compact in place” with an O(1)-space constraint.
  • “Partition by a predicate” — split into two or three categories without allocating.
  • “Is this a palindrome?” or any symmetric, compare-from-both-ends check.
  • Merge two sorted structures without extra space.
  • Process from the end backward — when a later element changes how you read an earlier one.
§ 02b — make the gap visceral

Brute force vs two pointers, racing

Same sorted array, same target. Step through it, or hit Play, and watch the comparison counters diverge. The right one barely moves while the left grinds through rows — that’s O(n) vs O(n²) as a count you watched accumulate, not a Big-O claim.

Widget A · race visualizer

opposite-direction
Brute force — every pair
comparisons
0
Two pointers — converge
comparisons
0
target
§ 03 — the three geometries

The three shapes of two pointers

There are three distinct geometries, and recognizing which one a problem wants is half the battle.

Opposite-direction (converging)Pointers start at both ends and move toward each other. Two Sum (sorted), Valid Palindrome, Container With Most Water, Trapping Rain Water. Trigger: a static, sorted-or-symmetric structure where the answer is a relationship between a far-apart pair. Invariant: “the answer, if it exists, is inside [lo, hi].”
Same-direction (fast / slow)Both pointers move forward, never back. slow/write marks the boundary of the finalized region; fast/read scans ahead for the next keeper. Trigger: “remove / dedupe / compact in place.” Invariant: “everything left of slow is the final answer so far.”
Multi-pointer partitionThree pointers carve the array into regions by a predicate in one pass — the Dutch national flag. Highest-value pattern: it generalizes to quicksort’s partition step and to query-engine row filtering. Treated on its own below.

The simulator steps through one variant of each of the first two shapes. Pick a problem, hit Step, and watch the color-coded regions — finalized (teal), the active window, and the pointer cells (amber) with their role labels. The same visual vocabulary describes structurally different problems.

Widget B · pointer simulator

step-through
finalized / matched active window pointer

Three things to notice. In two-sum, cells outside [L, R] dim out — the invariant erasing proven-dead candidates. In remove-duplicates, the teal prefix is the entire answer; everything past it is scratch space you’re allowed to clobber. In valid-palindrome, outer cells turn matched-teal as the pointers march inward — the converging frontier is the only place work happens.

§ 04 — the most generalizable pattern

The Dutch national flag

Dijkstra’s problem: given elements of three kinds — red / white / blue, or <pivot / ==pivot / >pivot — sort them in a single pass with O(1) space. Three pointers maintain four regions at all times. Memorize it as a picture:

the invariant, as four regions
[ 0 .. low )      < pivot   reds, finalized
[ low .. mid )    == pivot  whites, finalized
[ mid .. high ]   unknown   not yet examined
( high .. n )     > pivot   blues, finalized

mid is the scanner. The logic falls straight out of the invariant: a value below pivot swaps into the red zone (low++ mid++); a value equal to pivot is already placed (mid++); a value above pivot swaps to the blue zone (high--) and — the one rule everyone gets wrong — does not advance mid, because the element pulled in from high is unexamined.

Widget C · 3-way partition

dutch national flag
< pivot (red) = pivot (white) > pivot (blue) dashed = unknown zone
why mid holds after a high-swap

The element you just received from the high side has never been examined — so you must look at it next. Advance mid there and you skip a value, breaking the partition. That single restraint is the difference between a correct one-pass sort and a subtle bug.

§ 05 — draw the line now

Two pointers vs sliding window

The two are constantly confused. Two pointers works on a static structure: pointers converge from opposite ends or chase each other forward, and the answer is a pair, a partition boundary, or an in-place rearrangement. Movement is driven by a comparison against a fixed target or predicate.

Sliding window maintains a contiguous subarray with a property — longest substring without repeats, smallest subarray summing to ≥ K. Both edges move forward only; the right expands to admit elements, the left contracts to restore the property. The window’s contents are the answer.

crisp test

Answer is “a pair of indices” or “a rearranged array” → two pointers. Answer is “a contiguous run whose contents satisfy something” → sliding window. The fast/slow variant is sliding window’s mechanical cousin, but it tracks a write boundary, not a property-constrained window.

§ 06 — beyond the interview

Where this lives in real systems

Two pointers is the in-place discipline that shows up wherever you move structured data through a pipeline without paying for copies. Consider a command-line tool that ingests tabular row-and-column data and runs a query pipeline over it: filter rows by a predicate, select columns, sort the result — minimizing copying as data flows through each stage.

Engineers building that kind of tabular-data-query CLI reach for two pointers in two concrete places. First, in-place partitioning: splitting rows into “passes the filter” and “fails the filter” before a sort step is structurally identical to the Dutch-flag partition — track region boundaries and swap rows into place rather than allocating a new collection. Second, boundary-tracking instead of reallocating: filtering rows by walking a write pointer over the same backing buffer mirrors the exact “don’t recompute, just advance a boundary” discipline the interview problems test.

same invariant, different enforcement

The discipline of minimizing clones while passing data through a filter→sort pipeline (ownership / borrowing in a systems language) is the systems-level cousin of the same two-pointer in-place mutation discipline tested in interviews — same invariant, different language guarantees enforcing it. In an interview you assert and defend the invariant out loud; under a borrow checker the compiler refuses to build until you’ve respected it.

§ 07 — calibrated easy → hard

Problem set — 10

Skewed toward custom-bank favorites at research-lab-tier loops: heavy on partition and greedy-proof problems, light on filler. Tap any card to expand. Four are flagged with the widget above that re-parameterizes onto them — reuse the visual, don’t relearn the picture.

§ 08 — language-agnostic

Pseudocode templates

Runnable-logic-correct, no hand-waving. The three shapes, one block each.

opposite-direction
function converge(a, target):           # a is sorted
    lo = 0
    hi = length(a) - 1
    while lo < hi:
        s = a[lo] + a[hi]               # or any monotonic condition
        if s == target:   return (lo, hi)
        else if s < target: lo = lo + 1      # smaller side proven doomed
        else:              hi = hi - 1      # larger side proven doomed
    return NONE
fast / slow · write vs read
function compact(a):
    n = length(a)
    if n == 0: return 0
    slow = 0                            # last index of finalized region
    for fast from 1 to n - 1:
        if keep(a[fast], a[slow]):      # e.g. a[fast] != a[slow]
            slow = slow + 1
            a[slow] = a[fast]           # in-place write at the boundary
    return slow + 1                     # length of finalized region
three-way partition · dutch flag
function three_way_partition(a, pivot):
    low = 0                             # a[0 .. low)   < pivot
    mid = 0                             # a[low .. mid) == pivot
    high = length(a) - 1                # a[mid .. high] unknown
    while mid <= high:
        if a[mid] < pivot:
            swap(a[low], a[mid]); low += 1; mid += 1
        else if a[mid] == pivot:
            mid += 1
        else:                           # a[mid] > pivot
            swap(a[mid], a[high]); high -= 1   # do NOT advance mid
§ 09 — curated, not a dump

Further reading

Loop-invariant reasoningCLRS (Introduction to Algorithms), Chapter 2 develops the initialization / maintenance / termination framework using insertion sort. That three-part structure is what lets you state a two-pointer invariant rather than gesture at it. For a lighter, competition-flavored take with a dedicated two-pointer section, Antti Laaksonen’s Competitive Programmer’s Handbook (free) is excellent.
Dutch national flag — origin & generalizationEdsger Dijkstra, A Discipline of Programming (1976). It’s worth more than its toy framing: the same three-way partition is the heart of robust quicksort on duplicate-heavy input (Bentley–McIlroy’s “fat partition”), where collapsing equal keys into a middle region avoids quicksort’s classic quadratic blowup. It generalizes from “sort three colors” to “partition any stream by a ternary predicate in one pass.”
Beyond interviewsThe two-pointer merge is the merge step of merge sort — two read pointers walking two sorted runs. The same idea powers the sorted merge join in database query engines: with both relations sorted on the join key, advance a pointer into each and match in a single linear sweep instead of nested-loop or hash join. And quickselect/quicksort’s partition is the in-place pointer discipline from problems 4, 5, and 7.
How to use this: before coding any candidate problem, force yourself to say the invariant out loud and name which of the three shapes it is. If you can’t, you don’t yet understand the problem well enough to write it — and that two-second check is exactly the habit that separates passing from pattern-matching under time pressure.