What this is
The technique works when the data has structure — usually sorted order — that lets you rule out large groups of candidates with a single comparison.
The Classic: Pair Sum
Find two numbers in a sorted array that sum to a target. Brute force checks all pairs: O(n²). Two pointers starts one at each end:
- Sum too small? Move the left pointer right — only larger values lie that way.
- Sum too large? Move the right pointer left — only smaller values lie that way.
- Exactly right? Done.
Each move eliminates an entire set of pairs from consideration without ever testing them. The pointers only ever move toward each other, so the whole search is one pass.
Why Sorting Is Important
The technique depends on knowing what moving a pointer does. On a sorted array, moving left rightwards can only increase the sum — that guarantee is what makes it safe to discard everything you skipped.
On unsorted data no such guarantee exists, and the method is simply wrong. If your input is not sorted you must either sort first (O(n log n)) or use a hash set instead (O(n) time, O(n) space).
The three patterns
| Pattern | Pointers | Typical use |
|---|
| Opposite ends | Start and end, moving inwards | Two-sum on sorted data, palindromes, container problems |
| Same direction | Both forward, at different speeds | Removing duplicates, partitioning in place |
| Fast and slow | One moves twice as fast | Cycle detection, finding the middle |
Same direction is the in-place editing pattern. One pointer reads, the other writes:
def remove_duplicates(arr): # arr is sorted; edit in place
if not arr:
return 0
write = 1
for read in range(1, len(arr)):
if arr[read] != arr[write - 1]:
arr[write] = arr[read]
write += 1
return write # new length
The invariant: everything before write is the deduplicated result so far. That framing — naming what is true about the region behind each pointer — is how these are reasoned about and debugged.
Fast and slow is the cycle-detection pattern, and the reason it works is worth understanding rather than memorising.
Container With Most Water
A subtler case worth understanding. Given heights, pick two lines forming the largest water container. Area is min(height) × width.
Start wide and always move the shorter line inward. Why is that safe? Moving the taller one can only reduce the width while the area stays capped by the same shorter line — so it can never improve. Moving the shorter one at least gives a chance of a taller limit. That single argument is what makes the greedy pointer movement correct.
Two indices instead of two loops
The two-pointer technique replaces a nested loop with two indices moving through the data, turning O(n²) into O(n).
It applies when the data has structure — usually sortedness — that tells you which pointer to move.
The canonical problem: find two numbers in a sorted array that sum to a target.
Brute force checks every pair: O(n²). With two pointers, one at each end:
Array [2, 7, 11, 15], target 18:
| left | right | Sum | Action |
|---|
| 2 | 15 | 17 | Too small — move left right |
| 7 | 15 | 22 | Too large — move right left |
| 7 | 11 | 18 | Found |
Three steps instead of six comparisons, and the saving grows with n. The logic is what makes it correct: if the sum is too small, the only way to increase it is a larger left value; if too large, a smaller right value. No possibility is skipped.
def two_sum_sorted(arr, target):
lo, hi = 0, len(arr) - 1
while lo < hi:
s = arr[lo] + arr[hi]
if s == target:
return lo, hi
if s < target:
lo += 1
else:
hi -= 1
return None
Guided tour
- Run pair sum and step through. Watch each move discard a whole block of pairs the brute force would have tested.
- Compare the two counters. Two pointers does roughly n comparisons where brute force does n²/2.