Visualizations / arrays

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.

▶ Run this code in DryRun

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

time O(n) for the scan, O(n log n) if you must sort first space O(1)

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.

Related

Two Sum, Properly ExplainedThe Sliding Window AlgorithmBinary Search VisualizationArray Rotation In Place

▶ Run this code in DryRun