Notice Period
week 1 · lesson

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.

nums = [1, 3, 4, 6, 8, 11, 13] · target = 10step 1/12
L1346811R13

Sorted array. L starts at the smallest value, R at the largest. Target sum: 10.

pseudocode
L, R = 0, len(nums) - 1
while L < R:
if nums[L] + nums[R] == target:
return (L, R)
elif nums[L] + nums[R] < target:
L += 1 # sum too small
else:
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:

SituationReach for
Data already sorted, or answer is about positions in sorted ordertwo pointers
Unsorted, need original indices, can't afford to sorthash map
O(1) extra space demandedtwo 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

1

In the converging pair-sum loop, the sum is too small. Why is moving the left pointer inward *safe* (no valid pair missed)?

2

Container-with-most-water moves the pointer at the *shorter* wall. Why?

3

When does a hash map beat two pointers for pair-finding?

4

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: