The Two Pointers Technique
Two indices walking toward each other can replace a nested loop, turning quadratic brute force into a single linear pass. The trick is not the two variables — it is the argument for why moving one of them is safe, which is where most people's intuition quietly breaks and where the wrong answers come from.
This isn't a canned animation. DryRun compiles and executes the code below, then draws every step from the real trace — the same values your machine would produce.
The code
vector<int> pairWithSum(vector<int>& a, int target) {
int lo = 0, hi = (int)a.size() - 1;
while (lo < hi) {
int sum = a[lo] + a[hi];
if (sum == target) return {lo, hi};
if (sum < target) lo++;
else hi--;
}
return {-1, -1};
}
int main() {
vector<int> a = {1, 3, 4, 7, 11, 15};
vector<int> r = pairWithSum(a, 15);
cout << r[0] << ' ' << r[1] << endl;
return 0;
}
Finds indices of two values in a sorted array that sum to the target — here 4 and 11, at indices 2 and 4.
How it works, step by step
Sortedness is what makes a move safe
If a[lo] + a[hi] is too small, then a[lo] paired with anything smaller than a[hi] is also too small — so a[lo] cannot be part of any solution and can be discarded entirely. That elimination argument is the whole technique; without sorted input it collapses.
Every step removes a candidate permanently
Each iteration either finds the answer or rules out one endpoint forever. Since there are n endpoints, the loop runs at most n times. Compare that to the nested loop, which re-examines pairs that were already eliminated.
The pointers never cross
lo < hi keeps them distinct, so an element is never paired with itself. If you need to allow that, the condition becomes lo <= hi — but for a sum-of-two-distinct-elements problem, allowing it produces wrong answers on inputs like target = 2 * a[i].
The search space is a rectangle you shrink
Think of all pairs (i, j) as a grid. Incrementing lo deletes a whole row; decrementing hi deletes a whole column. The pointers trace a staircase through the grid that visits O(n) cells instead of all n squared.
The same-direction variant
Not all two-pointer problems converge. Removing duplicates in place, or partitioning by a predicate, uses a slow write pointer and a fast read pointer moving the same way. The shared idea is that one index describes the answer built so far and the other scans ahead.
Complexity
Each iteration advances one pointer and neither ever moves backwards, so at most n iterations occur; the sort dominates when the input is not already ordered, which is why the technique shines on data that arrives sorted.
Common bugs
Using while (lo <= hi) for distinct pairs
When lo == hi, the code computes a[lo] + a[lo] and can report a pair made of a single element used twice. On input {1, 3, 7} with target 6 it wrongly returns index 1 twice. Use lo < hi unless the problem explicitly allows reuse.
Forgetting to sort, or sorting a copy
The technique is silently wrong on unsorted input — it returns not-found for pairs that exist, because the elimination logic is invalid. Equally common: sorting a local copy and then returning indices, which no longer correspond to the caller's array. If the problem wants original indices, sort (value, index) pairs or use a hash map instead.
Moving both pointers on every iteration
lo++; hi--; inside the loop body rather than in the branches skips pairs entirely. On {1, 2, 3, 4} with target 5 it checks 1+4, then 2+3 — fine — but on a six-element array it steps straight past valid combinations. Exactly one pointer moves per iteration.
Handling duplicates by accident
For problems that need all unique pairs rather than one, a naive loop emits the same pair repeatedly when the array has runs of equal values. After recording a match you must skip forward past the duplicates on both sides, not just advance by one. Watching lo and hi crawl through a run of identical values makes the missing skip obvious.
Each of these is easier to see than to describe — step through it and watch the values change. Run it in DryRun.
Frequently asked
When can I use the two pointers technique?
When the array is sorted (or can be) and moving one pointer lets you rule out a whole set of candidates. The test is whether you can prove a statement like: if this pair's sum is too small, no pair involving the left element can work. If that inference holds, two pointers turns a nested loop into a single pass.
Why is two pointers O(n) and not O(n squared)?
Because neither pointer ever moves backwards. lo only increases and hi only decreases, so together they can move at most n times before meeting. Each move permanently eliminates one element from consideration, unlike a nested loop which revisits pairs it has already ruled out.
What is the difference between two pointers and sliding window?
A sliding window is a two-pointer variant where both pointers move in the same direction and the region between them is the answer under construction. Classic two pointers usually has the pointers converging from opposite ends, and the region between them is the part still to be searched.
Does two pointers work on unsorted arrays?
The converging variant does not — its correctness depends entirely on order. The same-direction variant does work on unsorted data for problems like in-place removal or partitioning, because those never rely on comparing across the gap.