Techniques
Two Pointers
Two indices on one array — solve in O(n) what naive loops do in O(n²)
The two-pointers technique uses two indices that traverse a sequence (array, string, linked list) and update under specific rules. Common patterns — start-and-end pointers moving inward, fast-and-slow pointers detecting cycles, and equal-rate pointers maintaining a window. Turns many O(n²) brute-force solutions into O(n).
- TimeO(n) typically — each pointer crosses the array once
- SpaceO(1) — no auxiliary structures
- PatternsOpposite ends, fast-slow, sliding window, partition
- Vs brute forceO(n²) → O(n) on sorted-array problems
- Famous problemsTwo Sum sorted, Trap Rain Water, Reverse linked list, Partition
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
Three flavors of the two-pointer pattern
Opposite-ends (squeeze)
One pointer at the start, one at the end. They move inward based on some condition until they meet. Used for sorted-array problems and palindrome checks.
// Reverse an array in place
function reverse(arr) {
let l = 0, r = arr.length - 1;
while (l < r) {
[arr[l], arr[r]] = [arr[r], arr[l]];
l++; r--;
}
return arr;
}
// Check palindrome
function isPalindrome(s) {
let l = 0, r = s.length - 1;
while (l < r) {
if (s[l] !== s[r]) return false;
l++; r--;
}
return true;
}
// Two Sum on sorted array
function twoSum(arr, target) {
let l = 0, r = arr.length - 1;
while (l < r) {
const sum = arr[l] + arr[r];
if (sum === target) return [l, r];
if (sum < target) l++;
else r--;
}
return null;
}
Fast and slow (tortoise-and-hare)
Two pointers moving in the same direction at different speeds. Used to detect cycles in linked lists, find the middle of a list, or detect rotations.
// Detect cycle in linked list (Floyd's)
function hasCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
// Find middle of linked list (the slow pointer at end of fast's iteration)
function middle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// Find start of cycle (Floyd's part 2)
function cycleStart(head) {
let slow = head, fast = head;
// Phase 1: detect cycle
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
// Phase 2: reset slow to head; advance both at same rate; meet at start
slow = head;
while (slow !== fast) { slow = slow.next; fast = fast.next; }
return slow;
}
}
return null;
}
Same-direction, different purpose
Both pointers move forward — one advances based on some scan condition, the other follows when the condition's met. Used for in-place compaction and partitioning.
// Move all zeros to end (in-place)
function moveZeros(arr) {
let writePos = 0;
for (let readPos = 0; readPos < arr.length; readPos++) {
if (arr[readPos] !== 0) {
[arr[writePos], arr[readPos]] = [arr[readPos], arr[writePos]];
writePos++;
}
}
return arr;
}
// Remove duplicates from sorted array (LeetCode 26)
function removeDuplicates(arr) {
if (arr.length === 0) return 0;
let writePos = 1;
for (let readPos = 1; readPos < arr.length; readPos++) {
if (arr[readPos] !== arr[readPos - 1]) {
arr[writePos++] = arr[readPos];
}
}
return writePos;
}
Two pointers vs other techniques
| Two pointers | Sliding window | Hash map | Brute force | |
|---|---|---|---|---|
| Time | O(n) | O(n) | O(n) | O(n²) typical |
| Space | O(1) | O(window size or k) | O(n) | O(1) |
| Requires sorted input? | Often yes | Sometimes | No | No |
| Best for | Pair-finding on sorted, in-place rearrange | Subarray with property | Pair-finding on unsorted | Tiny inputs, when nothing else applies |
| Example problems | Two Sum sorted, palindrome, partition | Longest substring no-repeat, max k-window | Two Sum (any), anagrams | Last resort |
Famous two-pointer problems
Trapping Rain Water (LeetCode 42)
Given heights of bars, compute total water trapped between them after rain. Brute force is O(n²); two pointers solves in O(n) with O(1) memory.
function trap(heights) {
let l = 0, r = heights.length - 1;
let leftMax = 0, rightMax = 0;
let total = 0;
while (l < r) {
if (heights[l] < heights[r]) {
if (heights[l] >= leftMax) leftMax = heights[l];
else total += leftMax - heights[l];
l++;
} else {
if (heights[r] >= rightMax) rightMax = heights[r];
else total += rightMax - heights[r];
r--;
}
}
return total;
}
Insight — the side with the smaller height is the bottleneck. Process it (it can only catch as much water as its leftMax/rightMax) and move that pointer. Each side processed O(n) times total.
3Sum
Find all unique triplets that sum to zero. Sort the array, then for each i fix arr[i] and use two-pointers on the remaining range to find pairs summing to -arr[i]. O(n²) total — better than the brute O(n³).
function threeSum(arr) {
arr.sort((a, b) => a - b);
const result = [];
for (let i = 0; i < arr.length - 2; i++) {
if (i > 0 && arr[i] === arr[i - 1]) continue; // skip dupes
let l = i + 1, r = arr.length - 1;
const target = -arr[i];
while (l < r) {
const sum = arr[l] + arr[r];
if (sum === target) {
result.push([arr[i], arr[l], arr[r]]);
while (l < r && arr[l] === arr[l+1]) l++;
while (l < r && arr[r] === arr[r-1]) r--;
l++; r--;
} else if (sum < target) l++;
else r--;
}
}
return result;
}
Python implementations
def two_sum_sorted(arr, target):
l, r = 0, len(arr) - 1
while l < r:
s = arr[l] + arr[r]
if s == target: return [l, r]
if s < target: l += 1
else: r -= 1
return None
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast: return True
return False
def move_zeros(arr):
write = 0
for read in range(len(arr)):
if arr[read] != 0:
arr[write], arr[read] = arr[read], arr[write]
write += 1
return arr
Common two-pointer bugs
- Wrong loop termination.
while (l < r)for opposite-ends;while (l <= r)when including the meeting point. Off-by-one bugs cause missed last comparisons or out-of-bounds access. - Forgetting to advance both pointers in equal cases. When two-pointer logic finds a match, you usually want to advance both — otherwise you re-process the same pair. Common in 3Sum and similar problems.
- Using two-pointers on unsorted data. The technique relies on monotonicity. On random data, the answer can be anywhere; pointer movement based on local comparison fails. Sort first or use a hash table.
- Cycle detection without the head-reset trick. Detecting cycle existence is one thing; finding the cycle's start requires a second phase (reset slow to head, advance both at same rate). Skipping it gives the meeting point, not the cycle start.
- Confusing two-pointers with sliding window. Sliding window has a specific structure — both pointers move forward, never back. Two-pointers is broader. Picking the wrong pattern leads to incorrect implementations or wasted complexity.
Frequently asked questions
When does two-pointers apply?
When the input has structure that lets you skip work — typically sorted arrays, sequences with monotonic properties, or doubly-linked structures. The two pointers exploit the structure to avoid the nested-loop comparison-of-all-pairs.
What's the difference between two-pointers and sliding window?
Sliding window is a special case of two-pointers — both pointers move in the same direction (left to right), maintaining a "window" between them. Generic two-pointers can move in opposite directions, at different rates, or independently. Sliding window is for "subarray with property X" problems; two-pointers is broader.
How does fast-and-slow pointer detect a cycle?
Floyd's tortoise-and-hare. Walk slow one step, fast two steps. If there's no cycle, fast hits null and you're done. If there's a cycle, the fast pointer eventually laps the slow one and they meet — guaranteed by the math (gap closes one position per step). O(n) time, O(1) space, no need to mark visited nodes.
Why does Two Sum work with two pointers on a sorted array?
With pointers at left=0 and right=n-1, sum = arr[left] + arr[right]. If sum < target, only increasing left can help (everything else is smaller). If sum > target, only decreasing right can help. So we always know which pointer to move, and each pointer moves at most n times — O(n) total. On unsorted arrays, you'd need a hash table to do it in linear time.
When can opposite-end pointers fail?
When the property you need isn't monotonic with the pointer movement. Example — finding a pair with maximum sum < X. If you move left right when sum is too small, you miss combinations where moving right left would've been correct. Always verify the monotonicity before applying the pattern.
How is the partition step in quicksort related to two pointers?
It's a two-pointer pattern. Lomuto partition uses one pointer for "where to put smaller elements" and another scanning the array. Hoare partition uses two pointers from opposite ends moving inward. Both rearrange in O(n) using O(1) extra memory — the canonical two-pointer technique applied to in-place rearrangement.
Are two pointers always faster than brute force?
Not always — only when the structure permits it. On unsorted arrays without auxiliary data, two-pointers gives no asymptotic improvement. Sort first if you can afford O(n log n); use a hash table if you can afford O(n) memory; use two-pointers when the input is already sorted or doubly-linked.