~/searching/two-pointers
Two pointers
Find two values that add up to a target in a sorted array with one pass from both ends. O(n) time, no extra memory.
Put one pointer at each end of a sorted array. If the sum is too small move the left one right; if too big, move the right one left.
The input is sorted (or can be), and you're looking for a pair, a triple, or a best pair of positions.
O(n)
O(1)
You’ll recognise it when
- The array is sorted, or sorting it doesn’t lose anything you need, and you want two values that add up to (or come closest to) a target.
- A brute force would try every pair, O(n²), and you need O(n).
- You’re asked for triples (3-sum) or the best pair of positions, like the widest or largest container.
- You must do it in place, with O(1) extra memory, so a hash map is off the table.
It’s often confused with the sliding window, where both pointers move the same way to track a contiguous stretch. Here they start at opposite ends and walk towards each other.
The idea
You and a friend want to spend exactly $20 on two different items from a price list sorted from cheap to expensive. Start with the cheapest and the priciest. Too expensive? The priciest item is hopeless: even with the cheapest item it’s over budget, so cross it off. Too cheap? The cheapest item is hopeless: even with the priciest item it falls short, so cross that off instead.
That’s the whole trick. Each comparison tells you which end can’t be in any pair, so you drop it and never look at it again. The two pointers close in until they find the pair or meet.
How it works
left starts at the first index and right at the last. Scroll through the steps and the graphic follows along; you can also press play, or edit the input and try a target with no pair.
- Start with the widest pair:
left = 0,right = n - 1. Every pair you could still pick lies between them. - Add the two ends:
total = a[left] + a[right], and compare it withtarget. - If
total > target, the sum is too big.a[right]overshoots even with the smallest value in play, so drop it:right -= 1. - If
total < target, the sum is too small.a[left]falls short even with the largest value in play, so drop it:left += 1. - If
total == target, returnleftandright. If the pointers meet first (left == right), there’s no pair: return-1 -1.
Why no pair is ever skipped: every pair that works stays between left and right. When total < target, a[right] is the largest value still in play, so a[left] plus anything else in range is also too small; a[left] belongs to no answer. The same argument in mirror image covers total > target. Each move throws away only a value that was provably useless.
def pair_with_sum(a, target):
"""Indices (i, j) with i < j and a[i] + a[j] == target, or (-1, -1).
a must be sorted in ascending order."""
left, right = 0, len(a) - 1
while left < right:
total = a[left] + a[right]
if total == target:
return left, right
# Drop the end that can't be part of any pair.
if total < target:
left += 1
else:
right -= 1
return -1, -1#include <utility>
#include <vector>
using namespace std;
// Indices {i, j} with i < j and a[i] + a[j] == target, or {-1, -1}.
// a must be sorted in ascending order.
pair<int, int> pairWithSum(const vector<int>& a, int target) {
int left = 0, right = (int)a.size() - 1;
while (left < right) {
long long total = (long long)a[left] + a[right];
if (total == target) {
return {left, right};
}
// Drop the end that can't be part of any pair.
if (total < target) {
left++;
} else {
right--;
}
}
return {-1, -1};
}class TwoPointers {
// Indices {i, j} with i < j and a[i] + a[j] == target, or {-1, -1}.
// a must be sorted in ascending order.
static int[] pairWithSum(int[] a, int target) {
int left = 0, right = a.length - 1;
while (left < right) {
long total = (long) a[left] + a[right];
if (total == target) {
return new int[] {left, right};
}
// Drop the end that can't be part of any pair.
if (total < target) {
left++;
} else {
right--;
}
}
return new int[] {-1, -1};
}
}Why it’s O(n)
Every step either returns or moves one pointer one place inwards, so the gap right - left shrinks by 1 each time. It starts at n - 1, which means at most n - 1 sums. Two integers of state make the space O(1).
Picture all pairs as a triangle of cells, row i and column j. Each move rules out a whole row (dropping left) or a whole column (dropping right), which is why n steps are enough to cover n² / 2 pairs.
| Approach | Time | Extra space |
|---|---|---|
| Try every pair | O(n²) | O(1) |
Binary search for target - x |
O(n log n) | O(1) |
| Hash set of values seen | O(n) | O(n) |
| Two pointers (sorted input) | O(n) | O(1) |
If the input isn’t sorted, sorting first costs O(n log n), and that dominates.
Common mistakes
Letting the pointers meet
With while left <= right, the loop still runs when both pointers sit on the same index, and that value gets paired with itself: target 4 “finds” 2 + 2 from a single 2.
while left <= right: # ✗ can use one element twice
while left < right: # ✓ two different positions
Overflowing the sum in C++ or Java
Two values near 2 × 10⁹ add up past the 32-bit limit and wrap to a negative number, which looks “too small” and moves the wrong pointer. Python’s integers don’t overflow.
int total = a[left] + a[right]; // ✗ wraps around
long long total = (long long)a[left] + a[right]; // ✓ fits
Sorting away the positions you need
If the problem wants the pair’s original positions, sorting the values scrambles them: left and right are indices into the sorted copy. Sort (value, index) pairs instead and report the stored index.
a.sort() # ✗ original positions lost
order = sorted((v, i) for i, v in enumerate(a)) # ✓ keep them alongside
Variations
- 3-sum. Sort, then fix each value as an anchor and run two pointers on the part after it, looking for
-anchor. O(n²) in total. Skip anchors equal to the previous one to avoid duplicate triples. - Container with most water. Pointers at both ends; the area is the shorter height times the width. Always move the shorter side: any container that keeps it is narrower and no taller, so it can’t win.
- Removing duplicates in place. Both pointers go left to right: a fast one reads every value, a slow one marks where the next unique value gets written.
- Reversing and palindromes. Swap or compare
a[left]anda[right], then step both inward until they meet. - Fast and slow pointers. On a linked list, one pointer moves two steps for each step of the other. When the fast one reaches the end, the slow one is at the middle; if they ever meet, the list has a cycle (Floyd’s algorithm).
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
The array is sorted, and you must say whether any two values add up to a target, using O(1) extra memory. What fits best?
Sorted input lets each comparison rule out one value for good, so the scan is O(n) time and O(1) memory. A hash set is also O(n) time but needs O(n) memory. Binary search per value works, but costs O(n log n).
-
2
This should list pairs of different positions that add up to 4. What does it print?
a, target = [1, 2, 3], 4left, right, out = 0, len(a) - 1, []while left <= right:total = a[left] + a[right]if total < target:left += 1elif total > target:right -= 1else:out.append((a[left], a[right]))left += 1right -= 1print(out)With
<=the loop still runs whenleft == right, so the single 2 at index 1 gets paired with itself. The condition must beleft < rightwhen the two values have to come from different positions. -
3
What does this 3-sum print?
nums = sorted([-1, 0, 1, 2, -1, -4])out = []for i in range(len(nums) - 2):if i > 0 and nums[i] == nums[i - 1]:continueleft, right = i + 1, len(nums) - 1while left < right:total = nums[i] + nums[left] + nums[right]if total < 0:left += 1elif total > 0:right -= 1else:out.append((nums[i], nums[left], nums[right]))left += 1while left < right and nums[left] == nums[left - 1]:left += 1print(out)Sorted, the list is [-4, -1, -1, 0, 1, 2]. Anchor -4 finds nothing (there’s only one 2). The first -1 finds (-1, -1, 2), then (-1, 0, 1). The second -1 is skipped by the
nums[i] == nums[i - 1]check; without it, (-1, 0, 1) would appear twice. -
4
In “container with most water”, why is it safe to always move the pointer at the shorter line inward?
The area is
min(h[left], h[right]) * (right - left). Pair the shorter line with anything further in and the width drops while the height can’t rise above that short line. So the short line is finished, and dropping it loses nothing. That makes the O(n) scan exact, not a guess. -
5
What’s the time complexity of 3-sum done as “sort, then for each anchor run two pointers on the rest”?
Sorting costs O(n log n). Then each of the n anchors runs an O(n) two-pointer scan, so O(n²) dominates. O(n² log n) is what you’d get by binary-searching for the third value instead of using two pointers.