latency.lab
Algorithms · Topic 02

Two Pointers

Level: foundationTool: index pairs, not nested loopsTime: ~55 min

By the end you can

Two pointers is the technique behind an entire class of problems where a nested loop feels natural but is actually wasted work — because the data has some structure (usually: it's sorted) that lets you rule out large chunks of the search space just by comparing.

What "two pointers" actually means

Instead of one index scanning the whole array while an inner loop re-scans it again, you keep two indices moving through the data, and you decide which one to move based on a comparison — never both blindly, never re-checking work you've already ruled out. Because each pointer only ever moves forward (never backward, never resets), the total number of steps across the *whole* algorithm is bounded by how far each pointer can travel — O(n), not O(n²).

The three shapes this technique actually takes

  • Opposite ends, converging inward. One pointer starts at index 0, the other at the last index; each step moves whichever side the comparison says can't possibly be part of a better answer. This needs the data to be sorted (or the comparison to be meaningful regardless of order, like heights in Container With Most Water). Two Sum II, Valid Palindrome, Container With Most Water, Trapping Rain Water, 3Sum, Squares of a Sorted Array.
  • Slow / fast, same direction. Both pointers start together and move the same way, but the slow one only advances when it needs to record something — this is how you compact or filter an array in place without extra space. Remove Duplicates, Move Zeroes.
  • One pointer per sequence. Two separate sequences (two arrays, or a string against another string), each with its own pointer, advancing independently based on a comparison between them. Merge Sorted Array, Is Subsequence.
1
2
4
7
11
15
▲left
▲right
⇒
1
2
4
7
11
15
▲left
▲right
Opposite-ends shape on [1, 2, 4, 7, 11, 15], target 15. Start: 1 + 15 = 16, too big → move right inward. Two moves later: 4 + 11 = 15, found — the faded cells are the ones each pointer has already ruled out and can never revisit.
The habit to build

Before coding, name which of the three shapes a problem is. "Sorted array, looking for a pair/triplet with some sum" is shape 1. "Rewrite this array in place, keeping only some elements" is shape 2. "Compare/merge two separate sequences" is shape 3. The shape tells you where the pointers start and what moves them — the code almost writes itself once you've named it.

1
3
0
0
12
▲slow
▲fast
Move Zeroes, mid-pass on [0, 1, 0, 3, 12] → currently [1, 3, 0, 0, 12]. fast has just found a non-zero (12) — swap it into slow's slot (dashed — the next write position), then advance slow. One more step finishes the array: [1, 3, 12, 0, 0].

Why moving the "wrong" pointer would break it

In the opposite-ends shape, the entire correctness argument rests on one claim: whichever side you move away from can be proven to never produce a better answer than what you already have. Container With Most Water makes this concrete:

why-the-shorter-side.cpp
// The container's water is capped by the SHORTER of the two walls.
// If height[left] < height[right], every container using "left" paired
// with anything between left+1 and right is STILL capped by height[left]
// -- and narrower, so worse. There's no point ever pairing "left" with
// anything closer than "right" again. Moving "right" instead would throw
// away the one candidate (the current "left") that might still pair
// better with something further in -- that's the bug.
if (height[left] < height[right]) ++left;   // left is the limiting side -- advance it
else --right;

This is the part that actually separates understanding the technique from pattern-matching it: being able to say why the pointer you moved was the correct one to move, not just that the code happens to pass.

Common mistakes

  • Using this technique on unsorted data when the opposite-ends shape needs sortedness to be valid — if the problem doesn't guarantee sorted input, you sort first (O(n log n)), which is still better than the O(n²) alternative, but it's an extra step people forget.
  • Off-by-one on the stopping condition — left < right vs left <= right changes whether the pointers are allowed to land on the same index, which matters when a single middle element needs to be considered on its own (binary search has the exact same trap).
  • Forgetting to skip duplicates in problems that ask for unique combinations (3Sum) — without it, the same triplet gets added multiple times as the pointers pass over repeated values.
  • Merging into a buffer from the front when you should merge from the back (Merge Sorted Array) — writing from the front overwrites values in the destination you still need to read.

Takeaways

  • Two pointers turns an O(n²) nested scan into O(n) by moving each pointer at most n times total, based on a comparison — never re-scanning what's already been ruled out.
  • Three shapes: opposite ends converging (needs sorted/orderable data), slow/fast same direction (in-place compaction), one pointer per sequence (merging/comparing two sequences).
  • The real skill is justifying why the pointer you moved can't produce a better answer — not just getting the code to pass.
  • Sorting first (O(n log n)) to unlock the opposite-ends shape is still a huge win over O(n²) — don't skip it just because the input wasn't already sorted.

Try it: opposite ends, converging

Two Sum II is the purest version of this shape — sortedness alone lets you drop the hash map from Arrays & Hashing entirely and still get O(n) time, now in O(1) space.

Live

Loading starter code…

Try it: slow/fast, in place

Move Zeroes is the cleanest version of the slow/fast shape: one pointer scans ahead, the other marks where the next kept value goes.

Live

Loading starter code…

Try it: proving which pointer to move

Container With Most Water is the one where you have to actually justify the move, not just pattern-match it — the walkthrough above is this exact problem.

Live

Loading starter code…

Keep going

That's 3 of the 12 problems in this topic. 3Sum and Trapping Rain Water are worth doing once these three feel automatic — they're the same shapes, just with an extra layer of reasoning on top. See the full Two Pointers set →

Checkpoint · Two Pointers

5 questions · pass at 70%

Q01
A problem gives you a sorted array and asks for a pair summing to a target. Which two-pointer shape is this?
WhySorted data plus "find a pair satisfying a sum condition" is the canonical opposite-ends shape: one pointer at each end, moved based on comparing the current sum to the target.
Q02
In Container With Most Water, if height[left] < height[right], why do you move "left" instead of "right"?
WhyThe shorter wall caps every possible container that uses it. Since you've already tried it against the widest possible partner (the current "right"), there's no narrower pairing that could beat it — so it can safely be advanced past.
Q03
What's the actual bug in doing Remove Duplicates or Move Zeroes with a single index instead of a slow/fast pair?
WhyIn-place compaction inherently needs two roles: one index scanning ahead (fast) looking for the next value worth keeping, and one index marking where to write it (slow). A single index can't do both jobs at once.
Q04
Why does Merge Sorted Array merge from the BACK of the destination array rather than the front?
WhyThe destination's own real values occupy its first m slots. Writing new (merged) values starting from the front would clobber those values before the algorithm has had a chance to compare them.
Q05
3Sum sorts the array first. What complexity does that make the overall algorithm, and why not skip sorting?
WhySorting costs O(n log n) once. Then for each of the n choices of a fixed first element, a two-pointer sweep of the rest costs O(n) — n such sweeps is O(n²), which dominates the one-time O(n log n) sort. Skipping the sort would remove the ability to use two pointers at all, forcing the O(n³) brute force.

Finished Two Pointers?

Pass the quiz to complete it automatically.