Two Pointers
Let structure kill the inner loop.
Hashing removes the inner loop by remembering. Two pointers removes it by exploiting order: when data is sorted (or can be), two indices walking toward each other can rule out whole swaths of candidates per step. One pass, O(1) extra space, and a discipline — every move must be justified by an argument about what can be safely discarded.
01Why order changes everything
Take "find two numbers summing to target" on a sorted array. Brute force checks all pairs — O(n²). But sorted order gives you leverage: the smallest element sits on the left, the largest on the right.
Stand one pointer at each end and look at their sum:
- Too small? The left value is too weak — and pairing it with anything other than the current maximum would be even smaller. Every pair involving it is ruled out. Move left inward.
- Too big? The right value is too strong by the mirror argument. Move right inward.
Each comparison permanently eliminates one element from consideration. n elements, n steps, done — O(n) where brute force needed O(n²).
The deep idea: sorted order means one comparison tells you about many pairs at once. You're not searching the n² grid of pairs; you're walking its boundary.
Principle: in sorted data, ask "what does this comparison let me discard?" If each step discards something for good, you've got a linear algorithm.
02Converging pointers: the canonical loop
The skeleton you'll write a hundred times:
def pair_sum_sorted(nums, target):
lo, hi = 0, len(nums) - 1
while lo < hi:
s = nums[lo] + nums[hi]
if s == target:
return [lo, hi]
elif s < target:
lo += 1 # nums[lo] can't partner with anyone — discard it
else:
hi -= 1 # nums[hi] is too big for everyone — discard it
return [-1, -1]
The same converging skeleton powers a surprising range:
- Palindrome checks — compare ends, walk inward; any mismatch is fatal.
- Container with most water — move the shorter wall, because keeping it caps every future area it could form.
- 3Sum — fix one element, run converging pointers on the rest. One pattern nested inside a loop turns O(n³) into O(n²).
In every case the loop is mechanical; the interview content is the one-sentence proof of why the discarded element can never matter. Practice saying that sentence.
Sorted array. L starts at the smallest value, R at the largest. Target sum: 10.
L, R = 0, len(nums) - 1while L < R:if nums[L] + nums[R] == target:return (L, R)elif nums[L] + nums[R] < target:L += 1 # sum too smallelse:R -= 1 # sum too big
03Fast & slow: pointers at different speeds
Converging pointers start at opposite ends. The other family starts both at the beginning and moves them at different speeds — a preview of patterns you'll meet again with linked lists and sliding windows.
The workhorse version is the reader/writer pass for in-place editing. write marks the boundary of the cleaned-up region; read scans ahead:
def dedupe_sorted(nums):
write = 0
for read in range(len(nums)):
if read == 0 or nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1
return write # first `write` slots hold the deduped prefix
Everything before write is final; everything between write and read is dead weight already processed. Remove-duplicates, move-zeroes, filter-in-place — all the same two-speed scan, all O(n) time, O(1) space.
The same different-speeds idea, run on a linked list (fast moves two hops per slow's one), finds middles and detects cycles — Floyd's tortoise and hare. You'll meet it properly in the linked-list week; recognize the family now.
Principle: two pointers means two frontiers — what's settled and what's being explored. Keep their invariant ("everything left of write is clean") in your head and the code writes itself.
04Two pointers vs hashing: choosing your weapon
Both kill the inner loop. How to pick:
| Situation | Reach for |
|---|---|
| Data already sorted, or answer is about positions in sorted order | two pointers |
| Unsorted, need original indices, can't afford to sort | hash map |
| O(1) extra space demanded | two pointers (hashing pays O(n) memory) |
| Need all pairs/triples without duplicates (3Sum) | sort + two pointers — sorting makes duplicate-skipping trivial |
| One-shot membership / "have I seen this?" | hash map |
The honest trade-off: two pointers is O(1) space but needs order — if you must sort first, you pay O(n log n) and lose original indices. Hashing is O(n) one-pass on any order but costs O(n) memory and won't naturally enumerate deduplicated combinations.
In an interview, naming this fork is the move: "unsorted, so a hash map gives one pass — but if it were sorted, two pointers would do it with no extra memory." One sentence, two patterns, full marks.
Principle: hashing buys speed with memory; two pointers buys speed with order. Identify which resource the problem hands you for free.
✦Prove it — checkpoint quiz
In the converging pair-sum loop, the sum is too small. Why is moving the left pointer inward *safe* (no valid pair missed)?
Container-with-most-water moves the pointer at the *shorter* wall. Why?
When does a hash map beat two pointers for pair-finding?
In the reader/writer (fast/slow) in-place pattern, what is the loop invariant?
⚒Now forge it
Reading is scaffolding. These drills are the building. Easiest first: