🚀 Phases 1–5 are live — Days 1–17 cover the foundations and the algorithmic patterns. See the roadmap →
Day 25 - Two PointersOpposite-Ends Pointers

Opposite-Ends Pointers

The most common two-pointer flavor. l starts at 0, r starts at n - 1, and they walk toward each other based on a comparison. The whole technique is built on a single idea:

Moving one pointer in a known direction can’t skip a valid answer.

When that’s true, you can collapse a O(n²) “for every pair” brute force into a O(n) single pass.

The template

l = 0
r = n - 1
while l < r:
    compare arr[l] and arr[r]
    if (some condition):
        record the answer (optionally)
        l += 1 OR r -= 1 OR both
    elif (need a larger value):
        l += 1
    else:  # need a smaller value
        r -= 1

Three things vary problem-to-problem:

  1. The comparison — sum, equality, character match.
  2. When and what to record — return immediately, accumulate a max, push to an output list.
  3. The movement rule — which pointer moves based on which condition.

The rest is the same skeleton.

Worked example 1: Two Sum II (sorted)

Given a 1-indexed sorted array numbers and target t, find the two 1-indexed positions i < j with numbers[i] + numbers[j] = t. There is exactly one solution.

Time: O(n). Space: O(1).

The convergence argument:

  • If numbers[l] + numbers[r] < t: any pair starting at l with a smaller right value (< r) would be even smaller. So no answer uses l — advance it.
  • If numbers[l] + numbers[r] > t: any pair ending at r with a larger left value would be even bigger. So no answer uses r — retreat it.

Each move discards a row OR a column of the implicit n × n pair grid. After at most 2n − 1 moves, all valid pairs have been considered or excluded.

Worked example 2: Valid Palindrome

Given a string s, return true if it’s a palindrome after lowercasing and removing non-alphanumerics.

Time: O(n). Space: O(1) — no copy of the filtered string.

The inner while loops handle the “skip junk characters” requirement without breaking the linear bound: each character is visited at most once by l and at most once by r.

Worked example 3: Container With Most Water

You’re given n non-negative integers height[0..n-1] representing the heights of vertical lines. Find two lines that together with the x-axis form a container holding the most water.

Brute force: for every pair, area = min(height[l], height[r]) × (r − l). O(n²).

Two-pointer: start l, r at the ends. The area at any step is min(h[l], h[r]) × (r − l). To beat the current area, we’d need to either move past the shorter line (since it’s the bottleneck) or hope the wider container has both lines taller. Moving the shorter line inward is the only direction that could increase the area — moving the taller line only ever decreases width without raising the bottleneck.

Time: O(n). Space: O(1).

Why “move the shorter line” is correct: the area is bottlenecked by the shorter line. If we moved the taller one, the new bottleneck is at best the same (and could be smaller); the width is smaller; the area can only go down. So any improvement must involve moving the shorter line. We can safely skip every pair whose taller side is the current one. That’s the convergence argument for this problem.

Worked example 4: Three Sum (reduction)

Given an integer array nums, return all triplets [a, b, c] with a + b + c = 0 and no duplicate triplets.

The trick: sort, fix the first element, run two-pointer on the rest. This reduces 3Sum to “find pairs summing to −nums[i]”.

Time: O(n²). Space: O(1) extra (the output doesn’t count).

⚠️

Duplicate handling is the hardest part of 3Sum. Three places need it:

  1. After fixing i, skip the next i if nums[i] == nums[i - 1] (same first element gives identical triplets).
  2. After finding a valid triplet, advance l past all equal values.
  3. After finding a valid triplet, retreat r past all equal values.

Skip any of the three and you’ll emit duplicates. Skip both 2 and 3 and you’ll emit the same triplet n times for arrays with many zeros.

A summary table

ProblemWhen to move l (forward)When to move r (backward)
Two Sum II (sorted)sum < targetsum > target
Valid Palindromealways after match; skip non-alnumalways after match; skip non-alnum
Container With Most Waterheight[l] < height[r]height[l] >= height[r]
3Sum (inner)s < targets > target
Trapping Rain Water (preview)height[l] < height[r]height[l] >= height[r]

Notice how often the rule is the same shape: “move the side that’s smaller / weaker / not-yet-satisfied”. That’s the heuristic for almost every new opposite-ends problem.

Quick check

In Container With Most Water, you compute the area at (l, r). Why is it always correct to move the SHORTER line inward?
In Three Sum, why do we sort first?
A common Three Sum bug: emitting [-1, 0, 1] twice for input [-1, -1, 0, 1, 1]. What's missing?

Next: same-direction pointers — the read/write pattern for in-place transforms.