core idea
Two indices walk the data instead of one. Because each step permanently discards a chunk of the search space — a pair, a prefix, a wall — you check what looks like O(n²) combinations in a single O(n) pass with O(1) extra space. The pattern has three shapes, and interviews use all three:
| shape | pointers | typical trigger |
|---|---|---|
| opposite ends | left at 0, right at n−1, converging |
sorted array + pair/containment question |
| same direction | slow write, fast read chase forward |
in-place removal / compaction, “O(1) space” |
| expand from middle | both start at a center, walk outward | palindromes, symmetry around a point |
when to reach for it
- The input is sorted (or sortable without losing anything you need) and the question is about a pair, triple, or range.
- The words “in-place” or “O(1) space” appear anywhere in the problem.
- You are comparing a sequence with itself reversed, or growing outward from a center — palindromes, mirrors, symmetry.
- A brute force would be two nested loops where the inner loop only moves one way.
universal templates
Opposite ends — the shape you will write most often:
public int[] OppositeEnds(int[] sorted, int target)
{
int left = 0, right = sorted.Length - 1;
while (left < right) // strict <: when they meet, no pair remains
{
int sum = sorted[left] + sorted[right];
if (sum == target) return [left, right];
if (sum < target) left++; // sorted[left] is too small for *everyone* → discard it
else right--; // sorted[right] is too big for *everyone* → discard it
}
return []; // no pair exists
}
Same direction (read/write) — the in-place workhorse:
public int Compact(int[] nums)
{
int write = 0; // everything before write is the answer so far
for (int read = 0; read < nums.Length; read++)
{
if (Keep(nums[read]))
nums[write++] = nums[read]; // claim the slot, then advance
}
return write; // new logical length
}
Expand from middle — palindromes grow outward:
public (int Start, int Length) ExpandFromCenter(string s, int left, int right)
{
while (left >= 0 && right < s.Length && s[left] == s[right])
{
left--; // grow the mirror outward
right++;
}
return (left + 1, right - left - 1); // we overshot by one on each side
}
Every problem below is one of these three skeletons with a different comparison in the middle.
the one question to ask
Before moving a pointer, ask: “which move lets me discard options without ever discarding the answer?” If you can’t answer that, the pattern doesn’t apply — and that’s a signal to reach for a hashmap instead.
problems
Mirror pointers from both ends, skipping non-alphanumerics.
Opposite-end pointers; each comparison discards a whole set of pairs.
Same-direction read/write pointers — the in-place compaction template.
Expand from every center — the third pointer shape, on a top-five interview question.
Converging pointers with a greedy proof: always move the shorter wall.
Sort + outer loop + two pointers, with duplicate-skipping discipline.
The capstone: converging pointers carrying maxLeft/maxRight invariants.
cheat sheet — two pointers
recognize it
- sorted array + pair/sum/closest question → opposite ends
- "remove/keep elements in place", "O(1) space" → read/write pointers
- palindrome or mirror comparison → both ends inward, or expand from center
key tricks
while (left < right)strict — meeting means nothing left to compare- read/write invariant:
[0, write)is always the finished answer - 3Sum = sort + fix one element + two-pointer the rest
- skip duplicates by sliding a pointer while
nums[i] == nums[i - 1] - expand-from-center: try both odd (i, i) and even (i, i + 1) centers
common bugs
<=in the converge loop — comparing an element with itself- moving the wrong pointer on a tie (Container With Most Water: move the shorter wall)
- forgetting the array must be sorted for opposite-end sums
- returning values when the problem wants original indices — sorting destroyed them