Skip to content

Two Pointers

Mark as:

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):

  1. The data is sorted (or you may sort it) and you want a pair or triplet with some sum or property.
  2. The problem is about symmetry, like palindromes.
  3. A score depends on two positions and the width between them shrinks.

Same direction (a reader and a writer, both moving forward):

  1. You must modify an array in place with O(1) extra space.
  2. 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:

  1. Put left on 2 and right on 15. Sum is 17, which is too big.
  2. 15 is too large even with the smallest partner, so discard it: right moves in to 11.
  3. Sum 2 + 11 = 13, still too big. Discard 11.
  4. 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.

pairTemplate — go/twopointers/pointers.go
// 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:

1for left < right
Why:

The pointers must stay distinct: a pair needs two different elements. When they meet, every candidate has been ruled out.

2sum := sorted[left] + sorted[right]
Why:

Sortedness is what gives this value meaning: it is both the smallest and the largest total that left or right can still reach.

3left++ // too small
Why:

Even the biggest remaining partner could not reach the target, so sorted[left] is useless for the rest of the search. Discard it.

4right-- // too big
Why:

The 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.

Two Sum II (sorted input)
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
}
1/11

The real solution

TwoSumSorted
// 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
// 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
// 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
// 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
// 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:

Trapping Rain Water
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
}
1/58
Trap
// 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
// 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

ApproachTimeSpace
Brute force (every pair)O(n²)O(1)
Two pointers on sorted dataO(n)O(1)
Sort first, then two pointersO(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. 1.
    Valid Palindrome
    Compare mirrored characters and skip the ones that do not count.
    Easy
  2. 2.
    Move Zeroes
    No second array allowed. What marks where the next non-zero belongs?
    Easy
  3. 3.
    Two Sum II - Input Array Is Sorted
    Constant extra space, and the order is a gift.
    Medium
  4. 4.
    Container With Most Water
    Start with the widest pair. What is the only way to beat it?
    Medium
  5. 5.
    3Sum
    Fixing one number turns this into a smaller problem you have already solved.
    Medium
  6. 6.
    Sort Colors
    Three values, one pass, no counting sort.
    Medium
  7. 7.
    Trapping Rain Water
    Water at a bar is limited by the lower of two maxima: which side is already decided?
    Hard

Which pattern? Drills

Unlabeled problems — pick the pattern, then read why.

Question 1 of 5

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?