Two Pointers — Opposite Ends
Your very first algorithm pattern, explained from zero. We build the idea from a real-life story, prove it can never fail, and learn to spot it in five different problems.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- Now, the code — line by line
- Why does it never miss the answer?
- Why is it so fast?
- When should I reach for this? (the trigger list)
- The same trick in five disguises
- Disguise 1 — Reverse a string in place
- Disguise 2 — Is it a palindrome?
- Disguise 3 — Container with the most water (LeetCode 11)
- Traps that catch beginners
- Say it like a pro (interview one-liner)
- Check yourself
- Practice problems
Before we start
You have never studied a "pattern" before? Perfect. This is the right place to begin. By the end of this page you will be able to:
- See the two-pointer idea in your head, like a short movie.
- Explain out loud why it works — not just memorise code.
- Recognise it inside five problems that look completely different on the surface.
Read slowly. Where you see a Pause & Think box, actually stop and think. That five seconds of effort is what makes the idea stick.
Picture this first (no code yet)
A real-life story
You are at a small shop. In your hand is a gift card worth exactly ₹12 — and the rule is strange: you must buy exactly two items, and together they must cost exactly ₹12. Not ₹11. Not ₹13.
The items are lined up on a shelf, already arranged cheapest on the left, most expensive on the right:
₹1 ₹3 ₹5 ₹7 ₹9 ₹11
You point your left finger at the cheapest item (₹1) and your right finger at the priciest (₹11). You add them up: 1 + 11 = ₹12. Done on the first try.
That felt easy. But how did you know to start from the two ends? And what would you have done if the total was wrong? Hold that thought — we are about to turn this exact instinct into an algorithm.
The actual problem
Here is the coding version of the shop:
You are given a sorted array of numbers and a target value. Find two numbers in the array that add up exactly to the target.
Example:
array = [1, 3, 5, 7, 9, 11]
target = 12
answer = (1, 11) because 1 + 11 = 12
Simple to state. Now let's solve it the obvious way first — and feel why the obvious way is painful.
First, the slow way (so you feel the pain)
The most natural idea: try every possible pair. Pick the first number, then check it against every other number. Then pick the second number, check it against all the rest. And so on.
def two_sum_slow(arr, target):
n = len(arr)
for i in range(n):
for j in range(i + 1, n):
if arr[i] + arr[j] == target:
return (arr[i], arr[j])
return None
This works. But count the effort. For an array of n numbers, the number of pairs you check is roughly:
n = 10 → about 45 pairs
n = 1,000 → about 500,000 pairs
n = 100,000 → about 5,000,000,000 pairs (five billion!)
At 100 million checks per second, that last case takes almost a minute. The trick we build next finishes the same case in less than a thousandth of a second. Same problem — a hundred-thousand times faster. Let's earn that speed.
The turning point
Go back to the shop. The shelf was sorted. That one word — sorted — is a gift most beginners walk straight past. Let's not.
Pause & think
You put your fingers on ₹1 (left) and ₹11 (right). Suppose their total was too big — say the target was only ₹8, and 1 + 11 = 12 is over.
Which finger should move, and in which direction? And here's the deeper question: is there any pair using the ₹11 that could still work?
Think it through with me. The ₹11 is the most expensive item on the whole shelf. If pairing it with the cheapest item (₹1) already overshoots ₹8, then pairing it with anything else — all of which cost ₹1 or more — will overshoot even harder.
So the ₹11 is useless. It can never be part of a winning pair. We can cross it off forever and slide our right finger left, to ₹9.
The mirror image is just as powerful:
- If the total is too small, the item under the left finger is the cheapest available. Even paired with the most expensive item on the shelf it falls short — so it can never win. Cross it off and slide the left finger right.
Every single comparison lets us throw away one item for good. That is the whole secret.
The one idea to remember
The entire pattern in one sentence
Start with one finger at each end of a sorted list. Look at the total. Too big? Move the right finger left. Too small? Move the left finger right. Each move permanently removes the one number that cannot possibly help — so the answer, if it exists, is always still trapped between your two fingers.
That's it. That sentence is the algorithm. Everything below is just watching it happen and writing it down.
Watch it happen, frame by frame
Let's hunt for target = 10 in [1, 3, 5, 7, 9, 11]. Read one frame at a time. L is the left finger, R is the right finger.
Frame 1
index: 0 1 2 3 4 5
value: [ 1 3 5 7 9 11 ]
L R
sum = 1 + 11 = 12 → TOO BIG → move R left
The 11 is retired. It can never help. Slide the right finger in:
Frame 2
index: 0 1 2 3 4 5
value: [ 1 3 5 7 9 11 ]
L R
sum = 1 + 9 = 10 → EXACTLY RIGHT → answer is (1, 9) ✓
Found it in two steps instead of checking every pair. Now watch the other branch — the "too small" case — with target = 16:
Frame 1: L=1, R=11 → sum 12 < 16 → TOO SMALL → move L right
Frame 2: L=3, R=11 → sum 14 < 16 → TOO SMALL → move L right
Frame 3: L=5, R=11 → sum 16 = 16 → FOUND (5, 11) ✓
And if the two fingers ever bump into each other without finding the target? Then you have tried every number that could possibly matter — there is no such pair, and you stop.
Pause & think
Cover the answer below. In [2, 4, 6, 8, 10] with target = 14, walk the fingers yourself. Which pair do they land on, and how many steps did it take?
Check your trace
L=2, R=10 → sum 12 < 14 → too small, move L
L=4, R=10 → sum 14 = 14 → FOUND (4, 10)
Two steps. If you got (4, 10), you understand the mechanism.
Now, the code — line by line
We had two "fingers." In code, a finger is just an index — a number that says which position we are pointing at. left starts at position 0 (the first item). right starts at the last position.
def two_sum_sorted(arr, target):
left = 0 # left finger: first index
right = len(arr) - 1 # right finger: last index
while left < right: # keep going while the fingers haven't met
total = arr[left] + arr[right]
if total == target: # exactly right — we're done
return (arr[left], arr[right])
elif total < target: # too small — the left item is too weak
left += 1 # move left finger one step right
else: # too big — the right item is too strong
right -= 1 # move right finger one step left
return None # fingers met, nothing found
Read it against the story, line by line:
left = 0andright = len(arr) - 1— put a finger on each end.while left < right:— keep working only while the fingers have a gap between them. The moment they meet, every useful number has been tried. (We use<, not<=, because a number can't pair with itself.)total < target→left += 1— total too small, so retire the weakest number and move the left finger inward.total > target→right -= 1— total too big, so retire the strongest number and move the right finger inward.
That's the complete algorithm. Nine real lines.
Why does it never miss the answer?
This is the question that separates memorising from understanding. Let's make it airtight, in plain words.
Think of the two fingers as the walls of a shrinking box. At the start, the box [left … right] holds the entire array, so if an answer exists, it is obviously inside the box.
Now, the only way we ever move a finger is by retiring a number we have proven cannot be in any answer:
- We move
leftright only after proving the left number is too small to ever reach the target — even with the largest partner available. - We move
rightleft only after proving the right number is too big to ever reach the target — even with the smallest partner available.
So every move throws away a number that was guaranteed useless. We never throw away a number that could have been part of the answer. Therefore the answer — if it exists — is always still inside the box. The box only shrinks by removing junk. When we finally look at the right pair, it's still there waiting. It cannot be skipped.
Want the formal version? (optional — for the curious)
Claim. If a valid pair (i, j) with i < j exists, the loop returns it.
Invariant. At the top of every loop iteration, left ≤ i and j ≤ right.
- Start:
left = 0 ≤ iandj ≤ n−1 = right. ✓ - When we do
left += 1: we hadarr[left] + arr[right] < target. Since the array is sorted,arr[right] ≥ arr[j], soarr[left] + arr[j] ≤ arr[left] + arr[right] < target = arr[i] + arr[j]. That forcesarr[left] < arr[i], henceleft < i, so afterleft += 1we still haveleft ≤ i. ✓ - When we do
right -= 1: symmetric argument givesright > j, soright − 1 ≥ j. ✓
The invariant is never broken, so the pointers can never step over the answer. When they meet, either we already returned it or it never existed. ∎
Why is it so fast?
Look at the loop again. On every iteration, exactly one finger moves inward by one step. left only ever goes right; right only ever goes left. Together they can move at most n steps before they meet.
So the whole thing does about n units of work — written O(n), "grows in a straight line with the input." Compare that to the slow version's n²:
n = 100,000
slow (n²) → ~5,000,000,000 steps
fast (n) → ~100,000 steps
And we used only two integer variables of extra memory (left and right), no matter how huge the array — that's O(1) space, the best possible.
| Approach | Time | Extra memory |
|---|---|---|
| Check every pair | O(n²) | O(1) |
| Two pointers | O(n) | O(1) |
When should I reach for this? (the trigger list)
You won't be told "use two pointers." You have to recognise the smell of the problem. Reach for opposite-end two pointers when you notice:
- The array (or string) is sorted, or you're allowed to sort it first.
- You're asked to find a pair (or decide something about the two ends).
- The brute-force answer is "check every pair," i.e. O(n²), and you want faster.
- You're working with something that has two ends that can move toward each other — like a string you read from both sides.
See two or more of those together? Put a finger on each end and start moving inward.
The same trick in five disguises
Here is the real test of understanding: the same skeleton — two fingers at the ends, move inward by a rule — solves problems that look nothing alike. Notice the skeleton each time.
Disguise 1 — Reverse a string in place
To reverse "hello", swap the two ends, then move inward:
def reverse(s):
left, right = 0, len(s) - 1
while left < right:
s[left], s[right] = s[right], s[left] # swap the ends
left += 1
right -= 1
Same fingers, same "move inward until they meet." The rule is just "swap" instead of "compare a sum."
Disguise 2 — Is it a palindrome?
A palindrome reads the same forwards and backwards ("racecar"). So compare the two ends; if they ever disagree, it's not a palindrome:
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
Disguise 3 — Container with the most water (LeetCode 11)
Vertical lines of different heights; pick two to hold the most water. Water held = width × shorter of the two heights. Start as wide as possible and always move the shorter wall inward — the shorter wall is the bottleneck, so the taller one can't help.
def max_water(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
area = (right - left) * min(height[left], height[right])
best = max(best, area)
if height[left] < height[right]:
left += 1 # move the shorter (limiting) side
else:
right -= 1
return best
Different question, same instinct: each step retires the wall that can't improve things.
Level up — 3Sum (three numbers that sum to zero)
Once the two-finger idea is comfortable, most "3-number" problems are just: fix one number, then run two pointers on the rest.
def three_sum(nums):
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # skip duplicate anchors
left, right = i + 1, len(nums) - 1
need = -nums[i] # we want the other two to sum to this
while left < right:
total = nums[left] + nums[right]
if total == need:
result.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1 # skip duplicate answers
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif total < need:
left += 1
else:
right -= 1
return result
The inner while loop is exactly the pattern from this page. You already know it.
Traps that catch beginners
Watch out for these
- Forgetting the array must be sorted. The whole "retire the useless number" logic depends on it. If the input isn't sorted, sort it first (or use a different pattern).
- Writing
while left <= rightinstead ofleft < right. With<=, the two fingers can land on the same element and you'd "pair a number with itself." Use<. - Moving the wrong finger. Too big → move
rightdown. Too small → moveleftup. Say it out loud until it's automatic. - Forgetting the empty case. An array with fewer than two elements has no pair — the
whilesimply never runs, which is correct, but always keep it in mind.
| Bug | Fix |
|---|---|
while left <= right: | Use left < right — two distinct items |
Moving left when the sum is too big | Too big → move right (down); too small → move left (up) |
| Assuming sorted input | Check it; call arr.sort() if you're allowed |
Say it like a pro (interview one-liner)
"The array is sorted, so I'll use two pointers from opposite ends. If the pair's sum is too big I move the right pointer down; if it's too small I move the left pointer up. Each move eliminates a number that can't be part of any answer, so it's O(n) time and O(1) space."
Deliver that in an interview and you've shown you understand the why, not just the what.
Remember this forever
Two Pointers — Opposite Ends
One finger at each end of a sorted list. Look at the total:
- Too big → move the right finger left.
- Too small → move the left finger right.
- Just right → done.
Each move retires a number that can't possibly help, so the answer stays trapped between your fingers.
Cost: O(n) time, O(1) space · Trigger: sorted list + find a pair · Skeleton: while left < right: move inward by a rule
Check yourself
Answer these from memory before moving on. Retrieving the idea is what burns it in.
Why does the array have to be sorted?
Because our decision to retire a number relies on it being the smallest or largest still in play. If the sum is too big, we retire the right number knowing it's the biggest available — only true if the list is sorted. Without order, "too big" tells us nothing about which number to drop.
The sum is too small. Which pointer moves, and why?
The left pointer moves right. The left number is the smallest still available; even paired with the largest number it falls short, so it can never reach the target. Retire it and try the next-smallest.
Why is the loop condition `left < right` and not `left <= right`?
Because we need two different elements. If left == right, both fingers point at the same number, and a number can't pair with itself. < stops us exactly one step before that.
What makes it O(n) instead of O(n²)?
Each iteration moves exactly one finger inward by one step, and the fingers only travel toward each other. So there are at most n iterations total — a single pass — instead of checking all ~n²/2 pairs.
Practice problems
Do them in this order. The first three lock in the core idea; the last two are "boss levels" that reuse it.
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Two Sum II (sorted input) | Easy | The exact pattern from this page | LC 167 |
| Reverse String | Easy | Same fingers, rule = "swap" | LC 344 |
| Valid Palindrome | Easy | Compare ends, skip non-letters | LC 125 |
| Container With Most Water | Medium | Always move the shorter wall | LC 11 |
| 3Sum | Medium | Fix one number, two-point the rest | LC 15 |
| Trapping Rain Water | Hard | Two pointers + track each side's max | LC 42 |
When you can solve Two Sum II without looking, and explain out loud why each pointer moves, you have truly learned this pattern. Next up: Two Pointers — Same Direction, where both fingers move the same way to clean up an array in a single pass.