Two Pointers
One-liner: use two indexes that move through the data under a simple rule, so each step rules something out and a nested loop becomes a single pass.
The analogy
Two people walk into a long corridor of doors numbered in increasing order, one from each end, looking for two doors whose numbers add up to a target. Whenever the total is too small, the person at the low end steps forward; too big, the person at the high end steps back. Nobody ever walks backwards, and nobody checks a door twice.
Recognition signals
Two pointers has two main shapes, and each has its own signals.
Opposite ends (start at both ends, move inward):
- The data is sorted (or you may sort it) and you want a pair or triplet with some sum or property.
- The problem is about symmetry, like palindromes.
- A score depends on two positions and the width between them shrinks.
Same direction (a reader and a writer, both moving forward):
- You must modify an array in place with O(1) extra space.
- You are filtering, compacting or partitioning: "remove", "move", "keep only".
Step-by-step walkthrough
Take Two Sum II on sorted [2, 7, 11, 15] with target 9:
- Put
lefton2andrighton15. Sum is 17, which is too big. 15is too large even with the smallest partner, so discard it:rightmoves in to11.- Sum
2 + 11 = 13, still too big. Discard11. - Sum
2 + 7 = 9. Found it, positions 1 and 2.
Every step removed one element from consideration, so at most n steps happen in total.
Code template
The opposite-ends skeleton. The three branches are the whole idea.
// opposite-ends template on sorted data: each comparison rules out one element for good.
func pairTemplate(sorted []int, target int) (int, int, bool) {
left, right := 0, len(sorted)-1
for left < right {
sum := sorted[left] + sorted[right]
if sum == target {
return left, right, true
} else if sum < target {
left++ // too small: nothing paired with sorted[left] can reach target
} else {
right-- // too big: nothing paired with sorted[right] can reach target
}
}
return 0, 0, false
}Why each part exists:
for left < rightThe pointers must stay distinct: a pair needs two different elements. When they meet, every candidate has been ruled out.
sum := sorted[left] + sorted[right]Sortedness is what gives this value meaning: it is both the smallest and the largest total that left or right can still reach.
left++ // too smallEven the biggest remaining partner could not reach the target, so sorted[left] is useless for the rest of the search. Discard it.
right-- // too bigThe mirror argument: even the smallest remaining partner overshoots, so sorted[right] is discarded.
See it run
Watch the pointers close in on a real problem. Try the presets, including the one with no answer.
- left
- 0
- right
- 3
- sum
- —
- target
- 9
Start with one pointer at each end of the sorted array. Looking for a pair that sums to 9.
// TwoSumSorted: 1-based indices of two numbers in sorted input summing to target.
func TwoSumSorted(numbers []int, target int) []int {
left, right := 0, len(numbers)-1
for left < right {
sum := numbers[left] + numbers[right]
if sum == target {
return []int{left + 1, right + 1}
} else if sum < target {
left++
} else {
right--
}
}
return nil
}The real solution
// TwoSumSorted: 1-based indices of two numbers in sorted input summing to target.
func TwoSumSorted(numbers []int, target int) []int {
left, right := 0, len(numbers)-1
for left < right {
sum := numbers[left] + numbers[right]
if sum == target {
return []int{left + 1, right + 1}
} else if sum < target {
left++
} else {
right--
}
}
return nil
}Variations
Same direction, read and write — read visits every element; write marks the end of the kept prefix. In a sorted array, compare against the last kept value:
// RemoveDuplicates: in a sorted slice keep one of each value, in place.
// Same-direction pointers: read scans everything, write marks the end of the kept prefix.
func RemoveDuplicates(nums []int) int {
if len(nums) == 0 {
return 0
}
write := 1
for read := 1; read < len(nums); read++ {
if nums[read] != nums[write-1] {
nums[write] = nums[read]
write++
}
}
return write
}Partition with a swap — keep the elements you want at the front, preserving order, by swapping them forward:
// MoveZeroes: push zeros to the end, keeping the order of the rest.
func MoveZeroes(nums []int) {
write := 0
for read := range nums {
if nums[read] != 0 {
nums[write], nums[read] = nums[read], nums[write]
write++
}
}
}Fix one, scan the rest — reduce a triplet to a pair problem. Sort, fix nums[i], run the opposite-ends scan, and skip duplicates:
// ThreeSum: unique triplets summing to zero.
// Fix one number, then run the opposite-ends scan on the rest; skip duplicates.
func ThreeSum(nums []int) [][]int {
sort.Ints(nums)
out := [][]int{}
for i := 0; i < len(nums)-2; i++ {
if nums[i] > 0 {
break // everything after is positive too
}
if i > 0 && nums[i] == nums[i-1] {
continue
}
left, right := i+1, len(nums)-1
for left < right {
sum := nums[i] + nums[left] + nums[right]
if sum < 0 {
left++
} else if sum > 0 {
right--
} else {
out = append(out, []int{nums[i], nums[left], nums[right]})
left++
right--
for left < right && nums[left] == nums[left-1] {
left++
}
}
}
}
return out
}Greedy on a score — the area is width times the shorter wall. Width only shrinks, so only moving the shorter wall can possibly improve it:
// MaxArea: most water between two vertical lines.
// Width only shrinks, so the only way to improve is to move the shorter wall.
func MaxArea(height []int) int {
best, left, right := 0, 0, len(height)-1
for left < right {
h := min(height[left], height[right])
best = max(best, h*(right-left))
if height[left] < height[right] {
left++
} else {
right--
}
}
return best
}Two walls, one limiting — Trapping Rain Water. Water above a bar is min(tallest left, tallest right) − height. The side with the shorter wall already knows its answer, so settle that bar and move that pointer. Step through it:
- left
- 0
- right
- 11
- leftMax
- 0
- rightMax
- 0
- water
- 0
Two pointers start at the two ends. Water above a bar is limited by the SHORTER of the tallest walls on either side.
// Trap: water above a bar = min(tallest bar on its left, tallest bar on its right) - its height.
// Two pointers: the side with the SHORTER wall is the limiting side, so its water level is already known.
func Trap(height []int) int {
left, right := 0, len(height)-1
leftMax, rightMax, water := 0, 0, 0
for left < right {
if height[left] < height[right] {
leftMax = max(leftMax, height[left]) // the right wall is at least this tall, so leftMax limits
water += leftMax - height[left]
left++
} else {
rightMax = max(rightMax, height[right])
water += rightMax - height[right]
right--
}
}
return water
}// Trap: water above a bar = min(tallest bar on its left, tallest bar on its right) - its height.
// Two pointers: the side with the SHORTER wall is the limiting side, so its water level is already known.
func Trap(height []int) int {
left, right := 0, len(height)-1
leftMax, rightMax, water := 0, 0, 0
for left < right {
if height[left] < height[right] {
leftMax = max(leftMax, height[left]) // the right wall is at least this tall, so leftMax limits
water += leftMax - height[left]
left++
} else {
rightMax = max(rightMax, height[right])
water += rightMax - height[right]
right--
}
}
return water
}Symmetry check — compare from both ends, skipping characters that do not count:
// IsPalindrome: ignoring case and non-alphanumerics, does it read the same both ways?
func IsPalindrome(s string) bool {
alnum := func(c byte) bool {
return c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
}
lower := func(c byte) byte {
if c >= 'A' && c <= 'Z' {
return c + 32
}
return c
}
left, right := 0, len(s)-1
for left < right {
if !alnum(s[left]) {
left++
} else if !alnum(s[right]) {
right--
} else if lower(s[left]) != lower(s[right]) {
return false
} else {
left++
right--
}
}
return true
}Complexity
| Approach | Time | Space |
|---|---|---|
| Brute force (every pair) | O(n²) | O(1) |
| Two pointers on sorted data | O(n) | O(1) |
| Sort first, then two pointers | O(n log n) | O(1) – O(n) |
| 3Sum (fix one + pointers) | O(n²) | O(1) besides output |
The pointers move at most n steps combined. When sorting is needed first, sorting dominates the cost.
Common mistakes
Practice ladder
Ordered Easy to Hard. For each, state what lets you safely throw an element away.
- 1.Valid PalindromeEasyCompare mirrored characters and skip the ones that do not count.
- 2.Move ZeroesEasyNo second array allowed. What marks where the next non-zero belongs?
- 3.Two Sum II - Input Array Is SortedMediumConstant extra space, and the order is a gift.
- 4.Container With Most WaterMediumStart with the widest pair. What is the only way to beat it?
- 5.3SumMediumFixing one number turns this into a smaller problem you have already solved.
- 6.Sort ColorsMediumThree values, one pass, no counting sort.
- 7.Trapping Rain WaterHardWater at a bar is limited by the lower of two maxima: which side is already decided?
Which pattern? Drills
Unlabeled problems — pick the pattern, then read why.
You are given an array already sorted in non-decreasing order and a target. Return the 1-based positions of two numbers that add up to the target, using only constant extra space.
Which pattern?