Two pointers: a pattern, not a trick
Once you see the two-pointer pattern clearly, you start seeing it everywhere. Here is how I think about it.
The two-pointer technique shows up in a lot of interview problems. It is usually taught as a trick for specific problems — "use two pointers for sorted arrays" — which undersells it.
It is a pattern with a clear structure. Once you internalize the structure, you recognize where it applies.
#The core idea
Two pointers is useful when you need to find a pair (or group) of elements that satisfies a condition, and the data has enough structure to let you eliminate candidates without checking every pair.
The brute-force is O(n²): every pair with two nested loops. The two-pointer reduction to O(n) works when you can look at the current pair and decide which pointer to move to get closer to the target.
That decision — which pointer to move, and why — is the whole pattern.
#The canonical example: two-sum on a sorted array
Given a sorted array and a target, find two numbers that add to the target.
function twoSum(nums: number[], target: number): [number, number] | null {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) return [nums[left], nums[right]];
if (sum < target) left++; // need larger sum → move left pointer right
if (sum > target) right--; // need smaller sum → move right pointer left
}
return null;
}The decision logic is the key:
- Sum too small? The left element is too small. Move left pointer right to get a larger element.
- Sum too large? The right element is too large. Move right pointer left.
The sorted invariant guarantees every move eliminates a candidate that cannot possibly be the answer.
#The same structure, different problems
Remove duplicates from sorted array (in-place):
function removeDuplicates(nums: number[]): number {
if (nums.length === 0) return 0;
let slow = 0;
for (let fast = 1; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}Here the two pointers move in the same direction. slow tracks the last unique element; fast scans forward. When fast finds something new, slow advances and takes the value.
Valid palindrome:
function isPalindrome(s: string): boolean {
let left = 0;
let right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) return false;
left++;
right--;
}
return true;
}Both pointers move inward until they meet. The invariant: everything outside [left, right] has already been verified as matching.
#Container with most water
This one catches people because the decision requires a proof, not just intuition:
function maxArea(heights: number[]): number {
let left = 0;
let right = heights.length - 1;
let max = 0;
while (left < right) {
const area = Math.min(heights[left], heights[right]) * (right - left);
max = Math.max(max, area);
if (heights[left] < heights[right]) left++;
else right--;
}
return max;
}Why move the shorter bar? Area is limited by the shorter side. Moving the taller side decreases width without any chance of increasing height — the other side still limits you. Moving the shorter side might find a taller bar that compensates for the lost width.
The reasoning — "can moving this pointer possibly improve the result?" — is the question to ask every time.
#Recognizing the pattern
Two pointers usually applies when:
- The input is sorted (or can be sorted without changing the problem)
- You are searching for a pair or subarray satisfying a condition
- The brute force is O(n²) with nested loops over the same array
The question to ask: if the current pair does not satisfy the condition, do I know which element to swap out? If yes, two pointers is likely valid.
Two pointers is a binary search over pairs. The sorted structure guarantees that moving a pointer in one direction monotonically changes the relevant property — and that monotonicity is what makes the decision possible.
The pattern is not about having two index variables. It is about the invariant you maintain and the decision rule that preserves it. Get those right and the code writes itself.