Two pointers from opposite ends
Put one index at the start and one at the end of a list, and move them toward each other to check pairs without a nested loop.
O(n)Space O(1)16 practice problemsStep-through pictureThe idea
Many questions ask about pairs of items: two prices that add up to a gift card, the first and last letters of a word, the two walls that hold the most water. The slow way checks every pair with two loops, one inside the other. A list of 1,000 items has about 500,000 pairs.
The two-pointer way keeps two indexes. left starts at the first item and right starts at the last item. At each step you look at the pair nums[left] and nums[right], and a simple rule tells you which index to move inward. The indexes meet in the middle after at most n steps.
The rule only works when the list has some order you can trust. Most often the list is sorted: then moving left right makes the sum bigger, and moving right left makes it smaller. For a word, the order is simply the position of each letter.
An everyday example
Two people packing a seesaw
Kids stand in a line from lightest to heaviest, and you want two kids whose weights add up to exactly 60 kg. You call the lightest kid and the heaviest kid. If together they weigh too much, the heaviest kid is too heavy for anyone in the line, so they sit down. If together they weigh too little, the lightest kid is too light for anyone, so they sit down. Each call removes one kid, so you are done after one walk down the line.
| In the example | In the code |
|---|---|
| The lightest kid still standing | nums[left] |
| The heaviest kid still standing | nums[right] |
| Their weight together | const sum = nums[left] + nums[right] |
| Too light: the lightest kid sits down | left++ |
| Too heavy: the heaviest kid sits down | right-- |
From the slow way to two pointers from opposite ends
- Start with the obvious way: try every pair. Two loops,
iandj > i, checknums[i] + nums[j]againsttarget. For 7 numbers that is 21 pairs, and for 100,000 numbers it is about 5 billion. Worse, it ignores the one useful fact we were given: the list is sorted. - Use the order by looking at the two extremes first: the smallest item plus the largest item. Suppose that sum is too small. The largest item is the biggest partner the smallest item could ever get, and even that isn't enough. So the smallest item can't be in any answer pair, and you can drop it for good.
- The same reasoning works the other way. If the smallest plus the largest is too big, the smallest item is the kindest partner the largest item could get, and it still overshoots. So the largest item can't be in any answer pair. Either way, one check removes one item from the list of possibilities.
- Keep the items still in play as a range with two indexes:
leftfor its smallest item andrightfor its largest. Each check either finds the pair or moves one index inward, so after at mostn - 1checks the range is empty. That loop is two pointers from opposite ends.
How it works
- Sort the list if it isn't sorted yet and the answer allows it.
- Put
leftat index0andrightat the last index,nums.length - 1. - While
left < right, look at the pair:sum = nums[left] + nums[right]. - If
sumequals the target, you found the pair: return[left, right]. - If
sumis too small, the item atleftcan't be part of any answer, so moveleftone step right. - If
sumis too big, the item atrightcan't be part of any answer, so moverightone step left. - When
leftmeetsright, every possible pair has been ruled out: return[-1, -1].
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function pairWithSum(nums, target) {let left = 0;let right = nums.length - 1;while (left < right) {const sum = nums[left] + nums[right];if (sum === target) return [left, right];if (sum < target) {left++;} else {right--;}}return [-1, -1];}
How fast it is
| Time | O(n) |
|---|---|
| Extra memory | O(1) |
Each step moves one pointer one place closer to the other, so the loop runs at most n - 1 times. It only keeps two indexes and a sum. Add O(n log n) if the list must be sorted first.
- Best case
O(1)The first and last items already add up to
target, like[2, 6, 9, 15]withtarget = 17: the first check returns. - Average case
O(n)The pair sits somewhere in the middle, so the pointers walk part of the way before meeting it. Each step costs one addition and one comparison.
- Worst case
O(n)No pair adds up to
target, so the pointers walk until they meet: exactlyn - 1checks. If the list must be sorted first, the total isO(n log n).
Common mistakes
- Using it on an unsorted list for a sum question. The rule "too small, so move
left" is only true when the items are sorted. Sort first, or use a hash map instead. - Writing
while (left <= right). When the pointers are equal, the loop pairs an item with itself, which can return a wrong pair like[2, 2]. - Sorting the list and then returning indexes when the question wants the original positions. Sort pairs of
[value, originalIndex], or return the values instead. - Moving both pointers after a mismatch in a sum question. Only one item is ruled out per step; moving both can skip the answer.
When to use it
Look for these clues in a problem:
- The input is sorted, or sorting it doesn't change the answer, and the question is about a pair of items.
- The question compares the start of a list or string with its end, such as "reads the same both ways".
- You must answer in one pass with
O(1)extra memory, so a hash map is not allowed. - The question asks for the best pair (closest sum, widest gap, most area) and a pair's value depends on the two ends.
Check yourself
Answer each one in your head first, then open it.
Why is left++ safe when the sum is too small?
nums[right] is the largest item still in play, and even it can't make nums[left] reach target. Every other partner is smaller, so nums[left] is in no answer pair.
What goes wrong with while (left <= right)?
When left === right, the loop pairs an item with itself. For [1, 4, 9] and target = 8, it would return [1, 1], using 4 twice.
At most how many checks does it make on 1,000 sorted numbers?
999. Each check either returns or moves one pointer one step closer to the other, and they start 999 apart.
Why can't you move both pointers after a mismatch in a sum question?
A mismatch proves only one item is useless. The other might still be part of an answer with a different partner, so moving it could skip the pair.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 16 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingeasy
medium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The loop keeps one promise: if some pair of indexes p < q has nums[p] + nums[q] === target, then such a pair lies inside the range, with left <= p and q <= right. At the start the range is the whole list, so the promise holds.
Each move keeps it. When sum < target, every q in the range has nums[q] <= nums[right], because the list is sorted, so nums[left] + nums[q] <= sum < target. No answer pair uses index left, and left++ drops only that index. When sum > target, the mirror argument shows no answer pair uses right, so right-- is safe.
The loop ends because right - left shrinks by one on every step that doesn't return. If it returns, the pair is a real match. If it ends with left === right, the range holds no pairs, so the promise says no answer pair exists, and [-1, -1] is correct.
Going deeper
3 more ideas
Why moving one pointer never skips the answer
Say nums[left] + nums[right] is smaller than the target. nums[right] is the biggest item still in play, so nums[left] plus any other item is even smaller. No pair that uses nums[left] can reach the target, so throwing left away is safe. The same reasoning, flipped, makes right-- safe when the sum is too big. Each step only throws away an item that can't be in any answer.
Why it takes O(n) time
Every step moves left right or right left by one. They start n - 1 apart and the loop stops when they meet, so there are at most n - 1 steps. If you must sort first, sorting costs O(n log n), and that becomes the total.
The same walk without a sorted list
Checking if a word reads the same both ways uses the same two indexes but a different rule. Compare word[left] with word[right]. If they differ, the answer is no. If they match, move both pointers inward. Here both pointers move every step, because a matched pair is finished.
Work it by hand
Every step in a table, with why
Find two numbers that add up to 32 in a sorted list of 7. The pointers move from both sides before they find the pair.
nums = [3, 5, 9, 14, 18, 21, 26], target = 32
| Step | left | right | nums[left] | nums[right] | sum | What happens | Why |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 26 | 29 | 29 < 32, so left++ | The smallest and largest items. Even 26 can't lift 3 to 32, so 3 is out. |
| 2 | 1 | 6 | 5 | 26 | 31 | 31 < 32, so left++ | Still too small: 5 is out for the same reason. |
| 3 | 2 | 6 | 9 | 26 | 35 | 35 > 32, so right-- | Now too big. Even the smallest item left, 9, makes 26 overshoot, so 26 is out. |
| 4 | 2 | 5 | 9 | 21 | 30 | 30 < 32, so left++ | The pointers can switch direction at any step: this time left moves again. |
| 5 | 3 | 5 | 14 | 21 | 35 | 35 > 32, so right-- | 21 is out: paired with the smallest item left, 14, it is already too big. |
| 6 | 3 | 4 | 14 | 18 | 32 | Match: return [3, 4] | 14 + 18 = 32, so the function returns both indexes. |
It returns [3, 4] after 6 checks. Checking every pair would have needed up to 21. The trace ends at the match; without one, it would end when left reached right.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Two pointers from opposite ends | O(n) | O(1) | The list is already sorted and you need a pair, a closest pair, or a count of pairs, with no extra memory. |
| Hash map of complements | O(n) | O(n) | The list is unsorted and you need the original indexes. Store each value's index and look up target - nums[i] as you go. |
| Sort, then two pointers | O(n log n) | O(n) | The list is unsorted, but you only need the values or a count, and you want the two-pointer walk's simplicity. |
| Check every pair | O(n^2) | O(1) | The list is tiny, or you need a quick, obviously correct version to test the fast one against. |
Extra memory: O(1): two indexes and one sum. Sorting a copy first, to keep the caller's array unchanged, would add O(n).
How the steps grow
- Two pointers from opposite ends, Hash map of complements
O(n) - Sort, then two pointers
O(n log n) - Check every pair
O(n^2)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Two pointers from opposite ends | 10 | 1,000 | 1 million |
| Hash map of complements | 10 | 1,000 | 1 million |
| Sort, then two pointers | 33 | 9,966 | 20 million |
| Check every pair | 100 | 1 million | 1 trillion |
Inputs that need care
5 tricky inputs
- An empty list or one item:
nums = []ornums = [4] rightstarts at-1or0, soleft < rightis false and the function returns[-1, -1]without a check.- The same item can't be used twice:
nums = [1, 4, 9],target = 8 4 + 4would be 8, butleft < rightkeeps the indexes different. The walk checks 10, then 5, then stops:[-1, -1].- Equal values at different indexes:
nums = [4, 4],target = 8 - They are two different items, so the first check returns
[0, 1]. - Several pairs work:
nums = [1, 2, 3, 4, 5, 6],target = 7 - It returns the first one it meets, the outermost pair
[0, 5]. To list every pair, save the match and move both pointers instead of returning. - An unsorted list:
nums = [9, 1, 5],target = 6 - The rule breaks:
9 + 5is too big, so it drops 5 and then misses1 + 5, returning[-1, -1]. Sort first, or use a hash map of complements.
Variations and follow-ups
Twists interviewers add
Closest sum
When no exact match is required, keep the sum with the smallest Math.abs(sum - target) and move the pointers by the same rule. The same proof shows the closest pair is never skipped.
Counting pairs below a target
If nums[left] + nums[right] < target, then nums[left] pairs with every index from left + 1 to right, so add right - left to the count at once and move left. This counts all such pairs in O(n).
Widest container
For walls of different heights, the water held is Math.min(height[left], height[right]) * (right - left). Always move the shorter wall: keeping it can never give a bigger area, since the width only shrinks.
Comparing ends of a string
Checking whether a word reads the same both ways uses the same two indexes but moves both after each match. Reversing an array in place swaps items[left] and items[right] and moves both inward too.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "sorted" plus "pair", or from a question that compares the start of a list with its end. If the list isn't sorted but the answer doesn't depend on positions, sorting first is often allowed.
- Explain the move rule with the reason, not just the rule: "if the sum is too small, even the largest item can't help
nums[left], sonums[left]can't be in any answer." That one sentence is the whole proof. - Test by hand: a list with no answer (the pointers should meet), a list of two items, and a target that needs the same item twice, which must not be allowed.
- Expect the follow-up "what if the list isn't sorted and you need the original indexes?". The answer is a hash map of value to index,
O(n)time andO(n)space, or sorting pairs of[value, index].
Where frontend code uses it
The same idea in real UI code
- Checking if a string reads the same both ways, such as a puzzle input or a mirrored icon name, is a two-pointer walk from both ends of the string.
- Reversing an array in place, which
Array.prototype.reversedoes without making a copy, swapsitems[left]anditems[right]while the indexes move inward. - A date range picker that needs two sorted prices, durations, or sizes that add up to a limit can find them with one inward walk instead of a nested loop.