◐ ai-teacher

🧩 Data Structures & Algorithms · Chapter 4

Two pointers

Checking every pair in a sorted list of 10,000 numbers is 50 million comparisons. Two fingers walking inward from the ends do it in under 10,000 — with no extra memory.

Study this lesson free in the app →

The basics

Two pointers is a technique where two indices move through an array (or string) together, each step deciding which one to move, so the whole job finishes in a single pass.

The problem it solves: many questions ask about pairs — two numbers that sum to a target, matching characters at both ends, two sorted lists to merge. Trying every pair is O(n²). If the data has structure — usually sorted order — each comparison tells you which pointer to move, and every move rules out an item for good. Total moves ≤ n, so O(n) time and O(1) extra space.

The mental model: paying exactly 30 with two items from a shelf sorted by price. Hold the cheapest in your left hand and the dearest in your right. Too expensive? Swap the right-hand item for the next cheaper one. Too cheap? Swap the left for the next dearer one. You never need to reconsider an item you've put back.

Words you must know 4

Opposite ends
left = 0, right = n - 1, move inward until they meet (pair sum, palindrome, reverse).
Same direction / read–write
a fast read pointer scans, a slow write pointer marks where the next keeper goes (remove duplicates in place).
Two arrays
one pointer per sorted list (merge).
Invariant
the fact that stays true every step and proves no answer was skipped.

Explained

"Is there a pair in this sorted list that adds up to 100?"

The obvious answer tries every pair: for 10,000 numbers, about 50 million checks.

Put one finger on the smallest number and one on the largest. Too big? Move the right finger left. Too small? Move the left finger right. At most 9,999 moves, and you've provably checked every pair that could work.

Two items, one exact budget

You have a gift card for exactly 30 and want to spend all of it on two items. The shop shelf is sorted by price: 4, 7, 11, 13, 19, 26.

Pick up the cheapest (4) and the dearest (26): 30. Done in one look.

Now say the shelf were 4, 7, 11, 13, 19, 28. Cheapest + dearest = 32, too much. The 28 can't work with anything — even the cheapest item overshoots — so put it back for good. Now 4 + 19 = 23, too little. The 4 can't work with anything left — even the dearest remaining item falls short — so put it back for good. 7 + 19 = 26, too little; drop the 7. 11 + 19 = 30.

Every look eliminates one item permanently. That's the whole trick.

Why it never misses an answer

Sorted array a, left = 0, right = n − 1, target T. Compare s = a[left] + a[right]:

Invariant: any valid pair lies entirely inside [left, right]. It's true at the start, and each step only throws away an element that can't be part of a solution. When left == right, no pair remains — so "not found" is a proof, not a guess.

Cost: each step moves one pointer inward, so at most n − 1 steps: O(n) time, O(1) space. Compare brute force at O(n²), or a hash set at O(n) time but O(n) space.

Pair found: target 11

1 + 19 = 20, too big: drop 19. 1 + 15 = 16, drop 15. 1 + 12 = 13, drop 12. 1 + 9 = 10, too small: now drop the 1. 2 + 9 = 11 — found in 5 steps. Notice the right pointer did most of the walking while the sums were too big.

No pair: target 18

Same array, target 18. The pointers zig-zag — 20, 16, 17, 19, 16, 19, 16 — until both land on 9 after 7 comparisons. No pair sums to 18, and because each step discarded an element that couldn't work, meeting in the middle proves it. Brute force would have needed all 28 pairs.

Three shapes of the pattern

def pair_sum(a, target):               # sorted input
    left, right = 0, len(a) - 1
    while left < right:
        s = a[left] + a[right]
        if s == target:
            return left, right
        if s < target:
            left += 1                    # need bigger
        else:
            right -= 1                   # need smaller
    return None                          # O(n) time, O(1) space

def is_palindrome(s):                  # opposite ends
    l, r = 0, len(s) - 1
    while l < r:
        if s[l] != s[r]:
            return False
        l += 1
        r -= 1
    return True                          # O(n) time, O(1) space

def merge(a, b):                       # one pointer per sorted list
    i = j = 0
    out = []
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            out.append(a[i]); i += 1
        else:
            out.append(b[j]); j += 1
    return out + a[i:] + b[j:]           # O(n + m) time and space

Complexity, and when to pick it

| Approach to pair-sum | Time | Extra space | Needs | | --- | --- | --- | --- | | Check every pair | O(n²) | O(1) | nothing | | Hash set of seen values | O(n) | O(n) | hashable values | | Two pointers | O(n) | O(1) | sorted input | | Sort, then two pointers | O(n log n) | O(1)–O(n) for the sort | can reorder |

My rule: already sorted → two pointers, no contest. Unsorted and you need original indices → hash map. Unsorted and memory is tight (a huge array on a small device) → sort in place, then two pointers.

The pattern also scales up: three-sum is "fix one item, two-pointer the rest" — O(n²) instead of O(n³) — and merging two sorted files of a billion lines each streams through both with constant memory, which is exactly how external merge sort and database merge joins work.

Gotchas

Words to know

Two pointersA technique where two indices traverse data together, each step choosing which to move, finishing in one pass.
Opposite-ends pointersStart at index 0 and n − 1 and move inward until they meet.
Read/write pointersA fast pointer scans every item; a slow pointer marks where the next kept item is written.
InvariantA condition that holds before and after every step — here, 'any valid pair lies between left and right'.
In placeModifying the input directly with only O(1) extra memory.

In two minutes

Test yourself

1. Two pointers for pair-sum works in O(n) mainly because…

  1. The array is sorted, so each comparison safely discards one element
  2. It uses a hash map behind the scenes
  3. It only looks at half the array
  4. It stops at the first element
Show the answer

The array is sorted, so each comparison safely discards one element — Sorted order is what makes 'too small → move left' safe. Each step removes one element from consideration, so at most n − 1 steps. It uses no hash map and can visit the whole array.

2. In a sorted array, a[left] + a[right] is smaller than the target. What do you do?

  1. right -= 1
  2. left += 1
  3. Move both pointers
  4. Restart from the beginning
Show the answer

left += 1 — You need a bigger sum. Moving left up gives a bigger (or equal) value; moving right down could only shrink the sum.

3. What are the time and extra-space costs of the two-pointer pair-sum on a sorted array?

  1. O(n²) time, O(1) space
  2. O(n) time, O(n) space
  3. O(log n) time, O(1) space
  4. O(n) time, O(1) space
Show the answer

O(n) time, O(1) space — One pass with two integer variables. O(n) time and O(n) space describes the hash-set approach; O(n²) is brute force.

4. You must check whether "racecar" reads the same backwards without building a reversed copy. Which approach?

  1. Sort the letters and compare
  2. Count the letters in a hash map
  3. Compare s[l] and s[r], moving both inward until they meet
  4. Binary search for the middle letter
Show the answer

Compare s[l] and s[r], moving both inward until they meet — Opposite-ends pointers compare matching positions in O(n) time and O(1) space. Letter counts can't tell 'racecar' from 'carrace', which isn't a palindrome.

5. You merge two sorted lists of lengths n and m with one pointer each. Time complexity?

  1. O(n + m)
  2. O(n × m)
  3. O((n + m) log(n + m))
  4. O(1)
Show the answer

O(n + m) — Each step appends one element and advances one pointer, so n + m steps total. The log factor would come from sorting the concatenation from scratch, which wastes the existing order.

6. The input is unsorted and the question asks for the indices of two numbers that sum to a target. Best approach?

  1. Sort, then two pointers, return the indices found
  2. One pass with a hash map of value → index
  3. Two pointers on the unsorted array
  4. Binary search for each element
Show the answer

One pass with a hash map of value → index — Sorting scrambles the indices, so you'd return positions in the sorted copy. A hash map keeps original indices: O(n) time, O(n) space. Two pointers on unsorted data misses pairs.

There are 9 questions, 4 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →