Watch the video version
Five and a half minutes, one film: the store story (the $18, the shelf over his head, a friend at the other end), then the code built line by line from the moves, running on the same shelf. The written version below adds the proof that it never misses, and a lab to try your own numbers.
Watch on YouTube1 · The problem$18 for exactly two things
Mom hands over eighteen dollars with one rule: buy exactly two things. And to spend all of it, those two things have to cost exactly $18 together.
At the store, the items stand in a row on the shelf, each with its price — and the prices are already in order, smallest to biggest: $2, $3, $4, $7, $11, $15, $20. Which two add up to eighteen?
Stated without the store: given a list of numbers sorted from smallest to biggest and a target, find the two positions whose numbers add up to the target. There is exactly one such pair, you can't use the same position twice, the positions are counted from 1, and you may only use a constant amount of extra memory.
2 · The obvious ideaThe obvious way: every pair
Try every pair. Take the $2 and try it with everything after it: 2 + 3, 2 + 4, 2 + 7, 2 + 11, 2 + 15, 2 + 20. None of them is 18. Next: take the $3 and try everything after it. Then the $4…
In code, two loops, one inside the other:
def two_sum_every_pair(numbers, target): for i in range(len(numbers)): for j in range(i + 1, len(numbers)): if numbers[i] + numbers[j] == target: return [i + 1, j + 1]
It works. On this shelf it finds $3 + $15 on its tenth check. But look at what it does along the way: the $15 is picked up, added to something, and put back again and again; so is the $20. You are checking the same numbers over and over.
With n prices there are n(n − 1) / 2 pairs — 21 for seven prices, about 5,000 for a hundred, about 50 million for ten thousand. That is O(n²): double the shelf and the work goes up four times.
3 · The observationBut wait — the prices are sorted
The every-pair loop would work just as well on a shuffled shelf. It never uses the one thing the problem gave us for free: the order. Maybe the order itself can help.
In a sorted list, the smallest number is at the left end and the biggest at the right end. If you add those two, you know something about every other pair: nothing can be smaller than the left end, nothing bigger than the right end.
4 · The discoveryStart at both ends
So instead of checking everything, start from the two ends: one hand — a left pointer — on the smallest price, the other — a right pointer — on the biggest. In the video he stands under the cheapest carton and calls a friend to stand under the dearest: the two of them are the pointers.
First try: 2 + 20 = 22. Too big. We need a smaller sum, and there is only one way to get one: the right pointer moves left, to a smaller price.
Second try: 2 + 15 = 17. Too small this time. We need a bigger sum, so the left pointer moves right, to a bigger price.
Third try: 3 + 15 = 18. Exactly eighteen. Three checks.
5 · The reasoningWhy each move is safe
Three checks instead of ten is nice, but it is only worth anything if we can't skip past the answer. Look at what each move throws away.
Too big. 2 + 20 = 22 is over the target. The $2 is the smallest price there is, so the $20 paired with anything else is bigger still. The $20 cannot be part of the answer at all — drop it. That is what moving the right pointer means.
Too small. 2 + 15 = 17 is under the target, and the $15 is the biggest price still in play. So the $2 paired with anything that is left is smaller still. The $2 cannot be part of the answer — drop it. That is what moving the left pointer means.
Because the numbers are sorted, every check tells you something about a whole group of pairs, not just the one you checked. Too big means the right number is useless with everything that is left; too small means the left number is. Either way one number leaves for good, and the answer is still between the pointers.
6 · The proofWhy it never misses: the pair grid
Draw every pair the every-pair loop would check as a grid: one row per left number, one column per right number. Only the squares above the diagonal are real pairs — 21 of them for seven prices.
The first check, 2 + 20, is the corner of the grid. Too big: the whole column of the 20 is crossed out — six pairs gone, with one check. The second check, 2 + 15, is the corner of what is left. Too small: the whole row of the 2 goes — five more. The third check is the answer.
That is the whole proof. The two pointers always look at the top-right corner of the pairs not yet ruled out, and that corner is special: it is the biggest pair in its row and the smallest in its column. Every check removes a full row or a full column of the remaining grid, and never a square that could still be the answer. With n numbers there are only n − 1 rows and columns to remove, so the search takes at most n − 1 checks.
7 · The costWhat it costs
Time: O(n). The left pointer only moves right, the right pointer only moves left, and they stop when they meet: together they take at most n − 1 steps, one check per step. Space: O(1). Two indices and a sum, whatever the size of the list.
If the list were not sorted, the usual fast answer is a hash map: walk once, and for each number ask "have I already seen 18 minus this?". That is also O(n) time, but O(n) memory for the map. Here, the sorted order does the remembering for us — that is why the memory can be constant.
8 · The nameThis is two pointers
Only now the name: two pointers. Two positions in the same list, each moving in one direction, every move decided by what the last check said.
- Start at both ends
Left on the smallest, right on the biggest.
- Check the sum
Add the two numbers you are pointing at.
- Too big? Move right in
The biggest number can't be used. Drop it.
- Too small? Move left in
The smallest number can't be used. Drop it.
- Found it? Stop
Return the two positions.
9 · The codeThe code, one move per line
Nothing new happens in the code: the same moves as in the store, written down. The function gets the sorted numbers and the target.
class Solution: def twoSum(self, numbers, target):
Start at both ends
Left points at the first position, right at the last.
class Solution: def twoSum(self, numbers, target): left, right = 0, len(numbers) - 1
Keep going while left is before right
class Solution: def twoSum(self, numbers, target): left, right = 0, len(numbers) - 1 while left < right:
Strictly less than, not less-or-equal. If the pointers meet, both point at the same position, and adding it to itself would use the same number twice — which the problem forbids.
Add the two numbers
while left < right: total = numbers[left] + numbers[right]
Found it? Stop
If the total is the target, return both positions — plus one each, because the problem counts positions from 1, while Python's indices start at 0.
while left < right: total = numbers[left] + numbers[right] if total == target: return [left + 1, right + 1]
Too big? Move the right pointer in
while left < right: total = numbers[left] + numbers[right] if total == target: return [left + 1, right + 1] elif total > target: right -= 1
Too small? Move the left pointer in
Otherwise the total is too small (it isn't equal, and it isn't bigger).
while left < right: total = numbers[left] + numbers[right] if total == target: return [left + 1, right + 1] elif total > target: right -= 1 else: left += 1
The whole thing
class Solution: def twoSum(self, numbers, target): 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: right -= 1 else: left += 1
Nine lines inside the function, and every lit one is a move from the store. Run it on the shelf:
| Check | left | right | total | Verdict |
|---|---|---|---|---|
| 1 | 0 → $2 | 6 → $20 | 22 | too big: right -= 1 |
| 2 | 0 → $2 | 5 → $15 | 17 | too small: left += 1 |
| 3 | 1 → $3 | 5 → $15 | 18 | found: return [2, 6] |
Two more runs to check yourself against: on [2, 7, 11, 15] with target 9 it returns [1, 2] after one check, and on [-1, 0] with target -1 it returns [1, 2] — negative numbers change nothing, only the order matters.
| In the store | In the code |
|---|---|
| Start at both ends | left, right = 0, len(numbers) - 1 |
| Add the two prices | total = numbers[left] + numbers[right] |
| Eighteen: stop | return [left + 1, right + 1] |
| Too big: right moves in | right -= 1 |
| Too small: left moves in | left += 1 |
10 · The trapsThree mistakes to watch for
while left <= rightlets the pointers meet and add a number to itself. On[3, 4]with target 6, it would answer 3 + 3.return [left, right]forgets the plus one: the answer is off by one position on both sides.- Moving the wrong pointer. On "too big",
left += 1makes the sum bigger. On the shelf it drops the $2 — fine — then sits on 3 + 20 = 23, too big again, drops the $3, and walks straight past the answer.
11 · Try itYour own numbers
Watch the pointers and the pair grid
The Python above, translated to run in your browser. Each check is a square of the grid; a "too big" crosses out its column, a "too small" its row. Change the numbers (they are sorted for you) and the target.
Things to try: a target no pair can make (the pointers meet and the loop ends — there would be no answer to return); a target made by the two smallest numbers (watch the right pointer walk all the way in); a long list, to see the gap between the two counters grow.
12 · The bigger ideaNot just for two numbers
The idea isn't really about finding two numbers. It is about ordered data: whenever things are sorted, two positions moving one way each can often walk through them without ever going back. Two sorted lists of user IDs, and you want the ones in both? Put a pointer at the start of each list; whichever points at the smaller ID moves on; when they match, you've found one.
Whenever you see two nested loops over the same list, ask: am I looking at the same elements twice? If the data is sorted — or you can afford to sort it — one check can often rule out a whole group, and one pass replaces the nest.
13 · PracticeFour problems, same shape
Don't copy the code. For each one, find the moves first: where do the pointers start, and what makes each one move?
- Warm-upTwo sum on a sorted list, from scratch
Close this page and write it. Then add a counter and check that it never needs more than n − 1 checks.
- PracticeValid palindrome
Does a sentence read the same backwards, ignoring spaces and punctuation? Two pointers at the ends again — but there is no sum. What decides the move?
- VariationContainer with most water
Vertical lines of different heights; which two hold the most water between them? Start at both ends. Which pointer can't possibly help any more — and why is it the shorter line?
- ChallengeThree numbers that sum to zero
List every set of three numbers in a list that adds up to zero, without repeats. Hint: sort first, fix one number, and the other two are a problem you just solved.
If you understand why the pointer moves, you can write the code.