Two Pointers Technique
The two pointers technique is one of the most practical tools in competitive programming and coding interviews. Instead of using a nested loop (O(n²)) to check every pair of elements, you maintain two index variables that move through the data in a coordinated way — cutting the work down to O(n).
The pattern appears in dozens of classic problems: finding pairs in sorted arrays, checking palindromes, removing duplicates, partitioning arrays, and more. Once you recognise the shape, solutions become straightforward.
The two core patterns
Every two-pointer problem falls into one of two shapes:
1. Opposite-end pointers — one pointer starts at the left, one at the right. They move toward each other until they meet. Best for problems where you want to process from both ends of a sorted array simultaneously.
2. Same-direction pointers (fast / slow) — both pointers start at the left, but one runs ahead of the other. Used when you want to maintain a condition about a window or partition as you scan forward.
Template 1: Opposite-end pointers
function twoPointerOpposite(arr) {
let left = 0;
let right = arr.length - 1;
while (left < right) {
// Process arr[left] and arr[right]
if (conditionMet(arr, left, right)) {
return [left, right];
} else if (shouldMoveLeft(arr, left, right)) {
left++;
} else {
right--;
}
}
return null; // not found
}Template 2: Same-direction (fast / slow) pointers
function twoPointerSameDirection(arr) {
let slow = 0; // "write" pointer — next valid position
for (let fast = 0; fast < arr.length; fast++) {
if (isValid(arr[fast])) {
arr[slow] = arr[fast]; // write valid value
slow++;
}
}
return slow; // length of valid prefix
}When to use two pointers
Sorted array, find a pair / triplet with target sum. Sorting + opposite pointers beats O(n²) brute force.
Palindrome or symmetry check. Compare characters from both ends inward.
Remove duplicates or filter in-place. Fast pointer scans, slow pointer writes.
Partition arrays. Dutch National Flag, moving zeroes, etc.
Merge two sorted arrays. Two pointers, one per array, advance the smaller.
Cycle detection in linked lists. Floyd's hare-and-tortoise algorithm.
Problem 1 — Two Sum II (sorted array)
Given a 1-indexed sorted array and a target, return the indices of the two numbers that add up to the target. Exactly one solution exists.
Leetcode 167
Brute force: O(n²) — check every pair.
Two pointers: O(n) — start at both ends, advance left when the sum is too small, retreat right when too large.
Two Sum II — O(n) time, O(1) space
function twoSum(numbers, target) {
let left = 0;
let right = numbers.length - 1;
while (left < right) {
const sum = numbers[left] + numbers[right];
if (sum === target) {
return [left + 1, right + 1]; // 1-indexed answer
} else if (sum < target) {
left++; // need a larger value
} else {
right--; // need a smaller value
}
}
return []; // guaranteed to find answer per problem constraints
}
// Examples
twoSum([2, 7, 11, 15], 9); // [1, 2] (2 + 7 = 9)
twoSum([2, 3, 4], 6); // [1, 3] (2 + 4 = 6)Why it works: the array is sorted. If the sum is too small, the only way
to increase it is to move left right (pick a bigger left value). If too
large, move right left. Each step eliminates at least one element from
consideration, so the loop runs at most n times.
Problem 2 — Valid Palindrome
Given a string, determine if it reads the same forwards and backwards after keeping only alphanumeric characters and lowercasing everything.
Leetcode 125
Opposite-end pointers are perfect: compare the outermost characters and move inward. If they differ at any point, it is not a palindrome.
Valid Palindrome — O(n) time, O(1) space
function isPalindrome(s) {
const isAlphanumeric = (c) => /[a-z0-9]/.test(c.toLowerCase());
let left = 0;
let right = s.length - 1;
while (left < right) {
// Skip non-alphanumeric from both ends
while (left < right && !isAlphanumeric(s[left])) {
left++;
}
while (left < right && !isAlphanumeric(s[right])) {
right--;
}
if (s[left].toLowerCase() !== s[right].toLowerCase()) {
return false;
}
left++;
right--;
}
return true;
}
// Examples
isPalindrome("A man, a plan, a canal: Panama"); // true
isPalindrome("race a car"); // false
isPalindrome(" "); // true (empty after filter)left < right to avoid the pointers crossing while skipping — that would make you compare the same character against itself.Problem 3 — Container With Most Water
Given an integer array height of length n, find two lines that together
with the x-axis form a container that holds the most water.
Leetcode 11
The volume of water between lines at index i and j is:
water = (j - i) * Math.min(height[i], height[j])
Greedy insight: start with the widest container (left = 0, right = n-1). Move the pointer at the shorter line inward — there is no point keeping the shorter line because it is already the bottleneck. Moving the taller line inward can only make things worse (width shrinks AND height stays bounded by the shorter line).
Container With Most Water — O(n) time, O(1) space
function maxArea(height) {
let left = 0;
let right = height.length - 1;
let maxWater = 0;
while (left < right) {
const width = right - left;
const currentWater = width * Math.min(height[left], height[right]);
maxWater = Math.max(maxWater, currentWater);
// Move the shorter wall inward
if (height[left] <= height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
}
// Example
maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7]); // 49 (between index 1 and 8)Problem 4 — 3Sum
Find all unique triplets in the array that sum to zero.
Leetcode 15
Strategy: sort the array, then for each element nums[i] run a
two-pointer search on the subarray to the right. This gives O(n²) overall —
far better than the O(n³) brute force.
Handling duplicates is the tricky part: skip duplicate values of i and
also skip duplicate inner pointer values after finding a valid triplet.
3Sum — O(n²) time, O(1) extra space (ignoring output)
function threeSum(nums) {
nums.sort((a, b) => a - b);
const result = [];
for (let i = 0; i < nums.length - 2; i++) {
// Skip duplicate values for the outer pointer
if (i > 0 && nums[i] === nums[i - 1]) continue;
// Early exit: smallest possible sum is already positive
if (nums[i] > 0) break;
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
result.push([nums[i], nums[left], nums[right]]);
// Skip duplicates for left and right
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
// Examples
threeSum([-1, 0, 1, 2, -1, -4]); // [[-1,-1,2],[-1,0,1]]
threeSum([0, 0, 0]); // [[0,0,0]]
threeSum([1, 2, -2, -1]); // []Problem 5 — Remove Duplicates from Sorted Array
Given a sorted array, remove duplicates in-place and return the length of the unique prefix. The relative order of elements must be preserved.
Leetcode 26
This is the classic fast/slow pattern. The slow pointer tracks where the next unique value should go; the fast pointer scans forward looking for a value different from the one just written.
Remove Duplicates — O(n) time, O(1) space
function removeDuplicates(nums) {
if (nums.length === 0) return 0;
let slow = 1; // next write position (index 0 is always kept)
for (let fast = 1; fast < nums.length; fast++) {
if (nums[fast] !== nums[fast - 1]) {
// New unique value found — write it at the slow position
nums[slow] = nums[fast];
slow++;
}
}
return slow; // length of unique prefix
}
// Example
const arr = [1, 1, 2, 3, 3, 3, 4];
const k = removeDuplicates(arr);
console.log(k); // 4
console.log(arr.slice(0, k)); // [1, 2, 3, 4]nums[fast] !== nums[slow - 2] instead of nums[fast] !== nums[fast - 1]. Generalize to at most k copies: compare nums[fast] !== nums[slow - k].Problem 6 — Dutch National Flag (Sort Colors)
Given an array of 0s, 1s, and 2s, sort it in-place in a single pass without using the built-in sort.
Leetcode 75
This classic problem by Dijkstra uses three pointers — a natural extension of two pointers. Maintain four regions at all times:
[0, low-1]— all 0s[low, mid-1]— all 1s[high+1, n-1]— all 2s[mid, high]— unexplored
The mid pointer scans forward. When it hits a 0, swap with low and
advance both. When it hits a 2, swap with high and retreat high only
(the swapped-in value is unexplored). When it hits a 1, just advance.
Sort Colors (Dutch National Flag) — O(n) time, O(1) space
function sortColors(nums) {
let low = 0;
let mid = 0;
let high = nums.length - 1;
const swap = (i, j) => {
[nums[i], nums[j]] = [nums[j], nums[i]];
};
while (mid <= high) {
if (nums[mid] === 0) {
swap(low, mid);
low++;
mid++;
// nums[low] was a 1 (already in 1s region), safe to advance mid
} else if (nums[mid] === 2) {
swap(mid, high);
high--;
// Do NOT advance mid — the swapped-in value is unexplored
} else {
mid++; // nums[mid] === 1, already in the right region
}
}
}
// Example
const colors = [2, 0, 2, 1, 1, 0];
sortColors(colors);
console.log(colors); // [0, 0, 1, 1, 2, 2]mid after swapping with high? When you swap nums[mid] with nums[high], the value now at nums[mid] came from the unexplored region — it could be a 0, 1, or 2. You must examine it before advancing.Complexity summary
Problem | Time | Space | Pattern |
|---|---|---|---|
Two Sum II | O(n) | O(1) | Opposite-end |
Valid Palindrome | O(n) | O(1) | Opposite-end |
Container With Most Water | O(n) | O(1) | Opposite-end (greedy) |
3Sum | O(n²) | O(1)* | Sort + opposite-end inner |
Remove Duplicates | O(n) | O(1) | Fast / slow |
Sort Colors | O(n) | O(1) | Three-pointer partition |
*O(k) extra space for the output list in 3Sum, where k is the number of triplets found. The algorithm itself uses O(1) extra space.
When NOT to use two pointers
Unsorted array, arbitrary pair constraints. Sorting costs O(n log n); if that is too slow, a hash map is usually faster.
Non-contiguous access patterns. Two pointers work because adjacent values are meaningful. Random-access problems need different approaches.
You need all pairs, not just one. Two pointers finds one optimal or valid pair efficiently. Enumerating all pairs is inherently O(n²).
Linked lists with arbitrary jumps. The fast/slow pattern works on linked lists for cycle detection, but random index access is not possible.
Common mistakes and edge cases
Off-by-one on termination. Use
left < right(strict), notleft <= right. When they meet on the same element you have nothing to compare.Forgetting to skip duplicates in 3Sum. Without the duplicate-skipping inner loops, you push the same triplet multiple times.
Moving the wrong pointer in Container With Most Water. Always move the pointer at the shorter wall — moving the taller one can never increase the area.
Not advancing mid after swapping with low in Dutch National Flag. When you swap a 0 to the front, the value that came from low was already processed (a 1), so mid can safely advance.
Array of length 0 or 1. Most two-pointer problems return trivially for tiny inputs; add the guard at the top.
Sorted order assumption violated. If you sort in a variant that needs the original indices, stash the indices as tuples before sorting.