Eliminate Nested Loops by Moving Two Indices Toward Each Other
Two-Pointer Pattern
The two-pointer technique collapses O(n²) brute-force solutions into O(n) by keeping two indices that advance based on a comparison, never revisiting the same pair.
What you'll learn
- Apply opposing pointers to solve two-sum on a sorted array and palindrome detection
- Apply same-direction pointers to remove duplicates and partition arrays
- Explain why two pointers achieve O(n) time and O(1) extra space
The two-pointer pattern uses two index variables that move through an array in a coordinated way. Because each pointer advances monotonically — never going backwards — the total number of steps across both pointers is at most 2n, giving an O(n) algorithm even when the naive approach is O(n²).
Opposing Pointers
Place one pointer at the start and one at the end. Advance or retreat based on a comparison at each step. The canonical problems are:
Two-Sum on a Sorted Array
Given a sorted array and a target, find two indices whose values sum to target.
function twoSum(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) return [left, right];
if (sum < target) left++; // need a larger value
else right--; // need a smaller value
}
return null; // no pair found
}
// Walk-through: nums = [1, 3, 4, 6, 9], target = 10
// left=0(1), right=4(9) → sum=10 → return [0,4]
console.log(twoSum([1, 3, 4, 6, 9], 10)); // [0, 4] Palindrome Check
function isPalindrome(s) {
let left = 0;
let right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) return false;
left++;
right--;
}
return true;
}
console.log(isPalindrome("racecar")); // true
console.log(isPalindrome("hello")); // false Same-Direction Pointers
Both pointers start at the left but move at different speeds. The “slow” pointer tracks where the next valid element should be written; the “fast” pointer scans ahead.
Remove Duplicates In-Place
function removeDuplicates(nums) {
// nums must be sorted
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; // length of deduplicated prefix
}
const arr = [1, 1, 2, 3, 3, 4];
const len = removeDuplicates(arr);
console.log(arr.slice(0, len)); // [1, 2, 3, 4] Complexity
| Variant | Time | Space | Notes |
|---|---|---|---|
| Opposing pointers (two-sum) | O(n) | O(1) | Requires sorted input |
| Opposing pointers (palindrome) | O(n) | O(1) | Works on any sequence |
| Same-direction (remove dups) | O(n) | O(1) | In-place, sorted input |
| Brute-force nested loops | O(n²) | O(1) | Baseline comparison |
The pattern always requires sorted data or a property (like symmetry) that makes it safe to discard elements when a pointer advances.
Up Next
When the “window” between two pointers has a variable width and you track a running aggregate, you have a sliding window — the next natural extension.
Sliding Window Pattern →