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 -= 1Three things vary problem-to-problem:
- The comparison — sum, equality, character match.
- When and what to record — return immediately, accumulate a max, push to an output list.
- 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
numbersand targett, find the two 1-indexed positionsi < jwithnumbers[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 atlwith a smaller right value (< r) would be even smaller. So no answer usesl— advance it. - If
numbers[l] + numbers[r] > t: any pair ending atrwith a larger left value would be even bigger. So no answer usesr— 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, returntrueif 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
nnon-negative integersheight[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]witha + b + c = 0and 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:
- After fixing
i, skip the nextiifnums[i] == nums[i - 1](same first element gives identical triplets). - After finding a valid triplet, advance
lpast all equal values. - After finding a valid triplet, retreat
rpast 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
| Problem | When to move l (forward) | When to move r (backward) |
|---|---|---|
| Two Sum II (sorted) | sum < target | sum > target |
| Valid Palindrome | always after match; skip non-alnum | always after match; skip non-alnum |
| Container With Most Water | height[l] < height[r] | height[l] >= height[r] |
| 3Sum (inner) | s < target | s > 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
Next: same-direction pointers — the read/write pattern for in-place transforms.