Sometimes you meet a problem that does not look like a two-pointer problem at all. You cannot just put one pointer at the start and one at the end and start moving them. But with a small, clever change — like sorting the array — the same problem suddenly *becomes* a two-pointer problem. This trick is called two-pointer reduction: you "reduce" a tricky problem into a familiar one you already know how to solve.
These problems are usually labelled *medium* or *hard*, because the change you need to make is not obvious, and you must be careful: the answer to the new (changed) problem must also be the correct answer to the original problem. If that is true, you can solve the easy version and trust the result.
Before you try a reduction, ask these questions. They help you discover whether a problem can be turned into a two-pointer problem:
Let's see these in action.
> Given an array arr of integers and a number target, find and return two elements whose sum equals target, if they exist.
For example, take this array and target = 13:
arr = [4, 1, 2, 7, 8, 5, 0, 6]
There are two pairs that add up to 13 here (for example 8 + 5). We want to find one such pair.
The simplest idea is to try every possible pair. Pick one item, then check it against every other item. If any pair adds up to target, return them.
vector<int> twoSum(vector<int> &arr, int target) {
// Use nested loop to find sum of all pairs
for(int i = 0; i < arr.size(); i++) {
for(int j = i + 1; j < arr.size(); j++) {
if(arr[i] + arr[j] == target)
return {arr[i], arr[j]};
}
}
return {};
}
This works correctly. But it uses a loop inside a loop. If the array has n items, we check about n × n pairs. We say this takes O(n²) time. For large arrays this is slow. Can we do better?
Let's answer our four questions for Two Sum:
After sorting:
arr = [0, 1, 2, 4, 5, 6, 7, 8]
Now place left at the start and right at the end. Because the array is sorted in non-decreasing order:
left to the right gives a bigger number, so the sum increases.right to the left gives a smaller number, so the sum decreases.This gives us full control over the sum, which is exactly what the two-pointer pattern needs.
At each step compute sum = arr[left] + arr[right], then:
sum == target → we found our pair. Return it.sum < target → the sum is too small. We need a bigger number, so increment left.sum > target → the sum is too big. We need a smaller number, so decrement right.Repeat while left < right.
#include <algorithm>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int> &arr, int target) {
// Sort the array in non-decreasing order
sort(arr.begin(), arr.end());
int left = 0;
int right = arr.size() - 1;
// Use a while loop to traverse the array using the two pointers
while (left < right) {
int sum = arr[left] + arr[right];
// Found a pair that sums up to the target
if (sum == target) {
return {arr[left], arr[right]};
}
// ... if sum < target, left++; else right--;
}
return {};
}
};
target = 13)| Step | left value | right value | sum | action |
|------|-----------|------------|-----|--------|
| 1 | 0 | 8 | 8 | 8 < 13 → move left right |
| 2 | 1 | 8 | 9 | 9 < 13 → move left right |
| 3 | 2 | 8 | 10 | 10 < 13 → move left right |
| 4 | 4 | 8 | 12 | 12 < 13 → move left right |
| 5 | 5 | 8 | 13 | 13 = 13 → found 5 and 8! |
Notice how the two pointers steadily close the gap between them. The array is scanned only once, so the loop runs in about O(n) time. (Sorting costs O(n log n), which is still far better than O(n²).)
The key idea: in every step we safely throw away one item forever, knowing it can never be part of the answer.
sum < target: arr[right] is currently the largest remaining number. If even pairing arr[left] with the largest number is still too small, then arr[left] paired with *any* other remaining number is also too small. So arr[left] can never reach target. We discard it by doing left++.sum > target: arr[left] is the smallest remaining number. If even pairing arr[right] with the smallest number is still too big, then arr[right] paired with *any* other remaining number is also too big. So arr[right] can never reach target. We discard it by doing right--.You might worry: "When we discard arr[right], did we miss its pair with an already-discarded item like arr[0]?" No — that pair was already checked back when arr[0] itself was discarded. So nothing is ever missed.
This is the invariant: we discard an item only after confirming none of its pairs can equal target. We keep going until we either find the pair, or all items are discarded (meaning no answer exists).
Two-pointer reduction is a whole family of problems. Once you can spot the reduction, many "hard" problems become routine. Examples include:
The big lesson: when a problem doesn't fit the two-pointer template directly, ask the four questions. Often a small change — most commonly sorting — unlocks the simple, fast solution.