Two pointers
One pass, two positions — how an O(n²) search collapses to O(n).
The shape of the pattern
Two pointers means walking a structure with two indices instead of
nested loops. Whenever you're tempted to check every pair
(i, j) against each other, ask: does the data
have an order I can exploit so the pointers only ever move forward, never
backward? If yes, two pointers turns O(n²) pair-checking into a single
O(n) pass.
Variant 1 — opposite ends, closing inward
Used on sorted arrays where you need a pair that satisfies some condition.
// Two Sum on a SORTED array — O(n) time, O(1) space
function twoSumSorted(nums, target) {
let left = 0, right = nums.length - 1;
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) return [left, right];
if (sum < target) left++; // need bigger → drop the smaller end
else right--; // need smaller → drop the bigger end
}
return [-1, -1];
}
Why this is correct, not just fast: because the array is sorted, moving
left past the current value can never re-find a pair we
already ruled out — every skipped pair genuinely can't work.
Watch it converge, step by step
nums = [2, 7, 11, 15, 18, 24], target = 22:
| step | left | right | nums[left]+nums[right] | compare to 22 | move |
|---|---|---|---|---|---|
| 1 | 0 (2) | 5 (24) | 26 | too big | right−− |
| 2 | 0 (2) | 4 (18) | 20 | too small | left++ |
| 3 | 1 (7) | 4 (18) | 25 | too big | right−− |
| 4 | 1 (7) | 3 (15) | 22 | match | return [1, 3] |
Six elements, but only four comparisons — each one eliminates an entire end of the remaining range, not just one element. That's the O(n) behavior: the pointers together take at most n steps total to meet.
Variant 2 — fast/slow, same direction
Both pointers start at the same end but move at different rates (or one waits while the other scans). This is the shape behind removing duplicates in place, partitioning, and cycle detection in linked lists.
// remove duplicates from a SORTED array, in place — O(n) time, O(1) space
function removeDuplicates(nums) {
if (nums.length === 0) return 0;
let slow = 0; // slow = last confirmed-unique position
for (let fast = 1; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1; // count of unique elements
}
Variant 3 — palindrome / mirror check
function isPalindrome(s) {
let left = 0, right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) return false;
left++;
right--;
}
return true;
}
How to recognize it in an unseen problem
- The input is sorted (or can be sorted without losing what you need)
- You're looking for a pair, triplet, or a "does X exist" over combinations
- A brute force would be nested loops comparing indices against each other
- The words "sorted array," "pair," or "in-place" appear in the prompt
Three Sum is the natural extension: sort once, then fix one index and run the opposite-ends two-pointer scan on the rest — O(n²) total instead of the O(n³) brute force, because the inner two-sum collapses from a nested loop to a linear scan.
See it move
Step through it and watch why a pointer moves. Every move rules out a whole block of pairs at once — that is the entire reason this beats the nested loop.
Opens in the editor — write it, run it, and check it against real tests.