10 Two Pointer Problems That Cover Every Interview Pattern
- Two pointer problems split into two templates: converging pointers (sorted arrays, opposite ends) and same-direction pointers (fast reader, slow writer), and every problem on this list is one of these two shapes
- The elimination argument is what makes converging pointers provably correct: moving the shorter pointer is the only change that can improve the answer, and interviewers probe this reasoning directly
- 3Sum is Two Sum II with an outer loop and deduplication bolted on; once you see it that way, 4Sum is just one more outer loop
- Sort Colors (Dutch National Flag) requires three pointers and a non-obvious rule: do not advance
midafter swapping from the high end, since the swapped-in value still needs inspection - Trapping Rain Water collapses two O(n) prefix arrays to O(1): when
height[left] < height[right], the left cell's water is fully determined byleft_maxalone - Minimum Window Substring uses the same expand-shrink loop as the other converging problems, but validity is a hash map count rather than a single comparison
You learn two pointers in an afternoon. You nod. You feel confident. Then an interviewer asks "why did you move the left pointer?" and you say "because...it felt right" and go silent for 45 seconds.
That silence is the gap. Two pointers gets taught as one pattern but it is actually three, and most prep resources never pull them apart. This list does.
Ten problems in deliberate order. Each one introduces exactly one idea the previous one did not. By problem 10 you have seen every situation two pointers shows up in during a real interview, plus the specific part that trips people up in each one.
Two Templates Power Everything Below
Two templates. Everything else is a variation on one of them.
Converging pointers start at opposite ends and move inward. They work on sorted arrays or symmetric structures. The only question that ever matters: which pointer do I move, and why is it safe to throw away what is behind it?
Same-direction pointers both start at the left. One reads ahead (fast), one writes or marks a boundary (slow). They cover in-place rewriting, cycle detection, and sliding windows.
Every problem below is one of these two shapes, sometimes with an extra pointer taped on for structural reasons.

1. Valid Palindrome (LC 125)
Difficulty: Easy. Teaches: The converging template, undiluted.
Left and right pointers march inward and skip non-alphanumeric characters. If they mismatch before crossing, return false. This is the purest version of the pattern. The whole topic lives in these 10 lines, so even if you could solve it in your sleep, read the code carefully.
def is_palindrome(s: str) -> bool: left, right = 0, len(s) - 1 while left < right: while left < right and not s[left].isalnum(): left += 1 while left < right and not s[right].isalnum(): right -= 1 if s[left].lower() != s[right].lower(): return False left += 1 right -= 1 return True
The double inner loop looks expensive. It is O(n) total because each character is visited at most once. That's worth being able to say out loud, because interviewers ask.
2. Move Zeroes (LC 283)
Difficulty: Easy. Teaches: Same-direction template, in-place writing.
A read pointer scans every element. A write pointer tracks where the next non-zero value should land. The read pointer always runs ahead. The write pointer only advances when it earns it. This asymmetry is the whole idea, and it shows up in three more problems on this list, so resist the urge to skim it.
def move_zeroes(nums: list[int]) -> None: write = 0 for read in range(len(nums)): if nums[read] != 0: nums[write] = nums[read] write += 1 while write < len(nums): nums[write] = 0 write += 1
After the loop, fill the remaining positions with zeros. Relative order of non-zero elements is preserved because read only ever moves left to right.
3. Remove Duplicates from Sorted Array (LC 26)
Difficulty: Easy. Teaches: The write pointer advances on a new value, not just a new position.
Sorted order means all duplicates are adjacent. You compare adjacent read values, not positions. Once you see that, the rest is just the same read-write pattern from problem 2 with a different condition.
def remove_duplicates(nums: list[int]) -> int: write = 1 for read in range(1, len(nums)): if nums[read] != nums[read - 1]: nums[write] = nums[read] write += 1 return write
write starts at 1. The first element is always unique and never needs to move. Same asymmetry as Move Zeroes. The pattern is repeating on purpose. By problem 6 it should feel automatic.
4. Two Sum II (LC 167)
Difficulty: Easy/Medium. Teaches: The elimination argument. Why converging pointers are provably correct.
The array is sorted. If numbers[left] + numbers[right] < target, moving right left can only decrease the sum further. Moving left right is the only change that could bring us closer to the target. This is not a heuristic or a gut feeling. It is a proof by elimination, and you need to be able to say it out loud without hesitating.
def two_sum(numbers: list[int], target: int) -> list[int]: left, right = 0, len(numbers) - 1 while left < right: total = numbers[left] + numbers[right] if total == target: return [left + 1, right + 1] elif total < target: left += 1 else: right -= 1
Every interviewer who asks Container With Most Water or 3Sum will probe this exact argument. Practice saying it out loud before you get to problem 5.
5. Container With Most Water (LC 11)
Difficulty: Medium. Teaches: The greedy elimination argument, harder version.
Same converging setup, tougher justification. Move the shorter wall inward. The container height is bounded by the shorter wall. Moving the taller wall inward shrinks the width and cannot gain height. Moving the shorter wall might find something taller, so it is the only move that could possibly improve the answer.
def max_area(height: list[int]) -> int: left, right = 0, len(height) - 1 best = 0 while left < right: water = min(height[left], height[right]) * (right - left) best = max(best, water) if height[left] < height[right]: left += 1 else: right -= 1 return best
"Why not move the taller wall?" is the follow-up that exposes whether you understand the proof or just memorized the code. Have the answer ready. It comes up nearly every time.
6. 3Sum (LC 15)
Difficulty: Medium. Teaches: Fix one pointer, run the inner two-pointer loop. Plus deduplication.
Sort first. Fix nums[i], then run Two Sum II on the subarray to the right. The deduplication is where candidates slip: skip duplicate outer values with continue, and skip duplicate inner values after recording each match. Getting the dedup right under pressure is the actual test here, not the outer structure.
def three_sum(nums: list[int]) -> list[list[int]]: nums.sort() result = [] for i in range(len(nums) - 2): if i > 0 and nums[i] == nums[i - 1]: continue left, right = i + 1, len(nums) - 1 while left < right: total = nums[i] + nums[left] + nums[right] if total == 0: result.append([nums[i], nums[left], nums[right]]) while left < right and nums[left] == nums[left + 1]: left += 1 while left < right and nums[right] == nums[right - 1]: right -= 1 left += 1 right -= 1 elif total < 0: left += 1 else: right -= 1 return result
3Sum is Two Sum II with an outer loop and deduplication bolted on. Once you see it that way, 4Sum is just one more outer loop. Most candidates know this going in. Far fewer get the dedup right when someone is watching.
7. Sort Colors (LC 75)
Difficulty: Medium. Teaches: Three pointers. The Dutch National Flag invariant.
Two pointers is not always two. Sort Colors needs three: low (left boundary of 1s), mid (current element), high (right boundary of 1s). The invariant: everything left of low is 0, everything right of high is 2, everything between low and mid is 1. Maintain that invariant at every step and the algorithm falls out.
def sort_colors(nums: list[int]) -> None: low, mid, high = 0, 0, len(nums) - 1 while mid <= high: if nums[mid] == 0: nums[low], nums[mid] = nums[mid], nums[low] low += 1 mid += 1 elif nums[mid] == 1: mid += 1 else: nums[mid], nums[high] = nums[high], nums[mid] high -= 1 # do NOT advance mid here
When you swap from the high end, do not advance mid. The swapped-in value came from the unknown zone and still needs inspection. This is the single most common bug on this problem. Interviewers know it and they wait for it.
See the Dutch National Flag deep dive for the full invariant proof and why the unknown zone shrinks by exactly one per iteration.
8. Trapping Rain Water (LC 42)
Difficulty: Medium/Hard. Teaches: Converging pointers as a DP space optimization.
The O(n) space solution precomputes left_max[i] and right_max[i] arrays. Two pointers eliminates both arrays. When height[left] < height[right], the right side is already at least height[right], which exceeds the left constraint. Water at the left cell is determined by left_max alone. You do not need to know the exact right maximum.
This feels like a magic trick the first time. It makes complete sense once you have seen the DP version.
def trap(height: list[int]) -> int: left, right = 0, len(height) - 1 left_max = right_max = 0 water = 0 while left < right: if height[left] < height[right]: if height[left] >= left_max: left_max = height[left] else: water += left_max - height[left] left += 1 else: if height[right] >= right_max: right_max = height[right] else: water += right_max - height[right] right -= 1 return water
Work the DP approach before this one if you have not seen it. The two-pointer version is much easier to derive when you already know what it is optimizing away.
9. Linked List Cycle II (LC 142)
Difficulty: Medium. Teaches: Fast-slow pointers. The Phase 2 reset math.
Phase 1: slow moves one step, fast moves two. If they meet, there is a cycle. Phase 2: reset slow to head, advance both one step at a time. They meet at the cycle entrance. Phase 1 is the party trick everyone knows. Phase 2 is where the math lives, and where interviewers love to ask "why does resetting slow to head actually work?"
def detect_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: break else: return None slow = head while slow is not fast: slow = slow.next fast = fast.next return slow
Use is not == for node comparison. Two distinct nodes with the same value are not the same node.
If the distance from head to cycle entrance is a and the cycle length is C, the Phase 1 meeting point is C - (a mod C) steps inside the cycle. After a more steps both pointers arrive at the entrance simultaneously. You do not need the derivation memorized, but you should be able to sketch why Phase 2 works.
See Floyd's cycle detection for Phase 1 and finding the cycle start for the full Phase 2 proof.
10. Minimum Window Substring (LC 76)
Difficulty: Hard. Teaches: Two pointers as a sliding window with auxiliary hash map state.
Expand right until the window contains all characters of t. Then shrink left to minimize. missing tracks how many required characters are still unmet. The left pointer only advances when the exiting character is surplus, not merely present. Read that sentence again. That is where most implementations go wrong.
from collections import Counter def min_window(s: str, t: str) -> str: need = Counter(t) missing = len(t) left = start = end = 0 for right, char in enumerate(s, 1): if need[char] > 0: missing -= 1 need[char] -= 1 if missing == 0: while need[s[left]] < 0: need[s[left]] += 1 left += 1 if not end or right - left < end - start: start, end = left, right need[s[left]] += 1 missing += 1 left += 1 return s[start:end]
The two-pointer mechanics are identical to the expand-then-shrink loop from Trapping Rain Water. The difference is what "valid" means. Here, valid means a hash map budget is satisfied, not a single comparison. Most people write this wrong two or three times before it clicks. That is completely normal. Write it wrong, understand the failure mode, try again.
Where Each Problem Fits
| Problem | Template | Core lesson |
|---|---|---|
| Valid Palindrome | Converging | Basic template |
| Move Zeroes | Same-direction | Read/write asymmetry |
| Remove Duplicates | Same-direction | Sorted write pointer |
| Two Sum II | Converging | Elimination argument |
| Container With Most Water | Converging | Greedy elimination |
| 3Sum | Converging + outer loop | Fix one, two-pointer inner |
| Sort Colors | Three pointers | DNF invariant, unknown zone |
| Trapping Rain Water | Converging | DP space optimization |
| Linked List Cycle II | Fast-slow | Cycle math, phase 2 |
| Minimum Window Substring | Sliding window | Auxiliary state validity |
The Part That LeetCode Cannot Train
Knowing which pointer to move is one thing. Explaining why it is safe to move it, in real time, while an interviewer is watching, is another. Two pointers is especially punishing on communication because the greedy choice at each step is non-obvious and interviewers probe it directly. A correct solution delivered in silence still gives the interviewer nothing to write in the feedback form.
The best way to close that gap is to practice explaining your reasoning out loud, not just getting the code right. SpaceComplexity runs voice-based mock interviews with rubric-based feedback on exactly this: whether you can articulate the elimination argument, state the invariant, and keep talking when asked a follow-up you did not see coming.
Further Reading
- LeetCode Two Pointers problem list - canonical set, filterable by difficulty
- Two Pointers Technique - GeeksforGeeks reference with visual examples
- Two Pointer Technique - the pattern reference with elimination proof
- Fast-Slow Pointer Problems - cycle detection pattern recognition