Two Pointers
Walk two indices through an array at once to replace a nested loop with a single pass.
How It Works
Plenty of array problems have an obvious O(n²) answer: try every pair. Two pointers gets the same answer in O(n) by keeping two indices and moving one of them each step, and neither index ever moves back.
It only works when you can tell, from the two values you're looking at, which pointer to move. That usually comes from sorted input. If the sum at left and right is too small, every pair that uses left with something before right is no bigger, so it's too small as well. left can never be part of the answer, and you move past it.
target = 10
[1, 3, 4, 6, 8, 11]
L R 1 + 11 = 12 too big -> R--
L R 1 + 8 = 9 too small -> L++
L R 3 + 8 = 11 too big -> R--
L R 3 + 6 = 9 too small -> L++
L R 4 + 6 = 10 found itPair Sum, Step by Step
Step 1 of 6
nums
- Pair being tried
- Ruled out
- Answer
Start left on the smallest value and right on the largest. You want two values that add up to 10.
All 6 steps as text
- Start
lefton the smallest value andrighton the largest. You want two values that add up to10. 1 + 11 = 12is too big. Every other partner for11is bigger than1, so11can't be in the answer. Moverightleft.1 + 8 = 9is too small. Every other partner for1is smaller than8, so1can't be in the answer. Moveleftright.3 + 8 = 11is too big. Every other partner for8is bigger than3, so8can't be in the answer. Moverightleft.3 + 6 = 9is too small. Every other partner for3is smaller than6, so3can't be in the answer. Moveleftright.4 + 6 = 10, the target. Return[2, 3].
There are two common shapes:
- Opposite ends. Start at the first and last index and move them toward each other. Used for pair sums, palindromes and the container problem below.
- Same direction. A slow pointer marks where the next kept element goes while a fast pointer scans ahead. Used to remove duplicates in place, partition an array, or find the middle of a linked list. When the stretch between the two pointers is what you care about, it becomes a sliding window.
When to Reach for It
- The input is sorted, or sorting it first won't break anything.
- You're looking for a pair or triplet with a target sum, difference or product.
- The question asks you to work in place with O(1) extra space.
- You're comparing the two ends of a string or array, as in a palindrome check.
Complexity
| Operation | Time | Space |
|---|---|---|
| Opposite-ends scan | O(n) | O(1) |
| Sort first, then scan | O(n log n) | O(1) to O(n), depending on the sort |
| Fix one element, scan the rest (3Sum) | O(n²) | O(1) beyond the output |
The Template
Pair With a Target Sum
Given a sorted array, return the indices of two numbers that add up to target. Start left at the smallest value and right at the largest. A sum that's too small means left is too small for every remaining partner, so move it right. A sum that's too big rules out right the same way.
Time O(n)Space O(1)
def pair_with_sum(nums: list[int], target: int) -> list[int]:
"""Indices of two numbers in sorted `nums` that add up to `target`,
or [-1, -1] if no pair does."""
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return [left, right]
if total < target:
left += 1 # Too small: only a bigger left value can help.
else:
right -= 1 # Too big: only a smaller right value can help.
return [-1, -1]
if __name__ == "__main__":
print(pair_with_sum([2, 7, 11, 15], 9)) # [0, 1]
print(pair_with_sum([1, 3, 4, 6, 8, 11], 10)) # [2, 3]
print(pair_with_sum([1, 2, 3], 7)) # [-1, -1]// Indices of two numbers in sorted `nums` that add up to `target`,
// or [-1, -1] if no pair does.
export function pairWithSum(nums: number[], target: number): [number, number] {
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++; // Too small: only a bigger left value can help.
else right--; // Too big: only a smaller right value can help.
}
return [-1, -1];
}
console.log(JSON.stringify(pairWithSum([2, 7, 11, 15], 9))); // [0,1]
console.log(JSON.stringify(pairWithSum([1, 3, 4, 6, 8, 11], 10))); // [2,3]
console.log(JSON.stringify(pairWithSum([1, 2, 3], 7))); // [-1,-1]package main
import "fmt"
// pairWithSum returns the indices of two numbers in sorted nums that add up
// to target, or [-1 -1] if no pair does.
func pairWithSum(nums []int, target int) [2]int {
left, right := 0, len(nums)-1
for left < right {
sum := nums[left] + nums[right]
switch {
case sum == target:
return [2]int{left, right}
case sum < target:
left++ // Too small: only a bigger left value can help.
default:
right-- // Too big: only a smaller right value can help.
}
}
return [2]int{-1, -1}
}
func main() {
fmt.Println(pairWithSum([]int{2, 7, 11, 15}, 9)) // [0 1]
fmt.Println(pairWithSum([]int{1, 3, 4, 6, 8, 11}, 10)) // [2 3]
fmt.Println(pairWithSum([]int{1, 2, 3}, 7)) // [-1 -1]
}using System;
public class Solution
{
// Indices of two numbers in sorted nums that add up to target,
// or [-1, -1] if no pair does.
public static int[] PairWithSum(int[] nums, int target)
{
int left = 0, right = nums.Length - 1;
while (left < right)
{
int sum = nums[left] + nums[right];
if (sum == target) return new[] { left, right };
if (sum < target) left++; // Too small: only a bigger left value can help.
else right--; // Too big: only a smaller right value can help.
}
return new[] { -1, -1 };
}
public static void Main()
{
Console.WriteLine(Format(PairWithSum(new[] { 2, 7, 11, 15 }, 9))); // [0, 1]
Console.WriteLine(Format(PairWithSum(new[] { 1, 3, 4, 6, 8, 11 }, 10))); // [2, 3]
Console.WriteLine(Format(PairWithSum(new[] { 1, 2, 3 }, 7))); // [-1, -1]
}
static string Format(int[] values) => "[" + string.Join(", ", values) + "]";
}import java.util.Arrays;
public class Solution {
// Indices of two numbers in sorted nums that add up to target,
// or [-1, -1] if no pair does.
static int[] pairWithSum(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) return new int[] {left, right};
if (sum < target) left++; // Too small: only a bigger left value can help.
else right--; // Too big: only a smaller right value can help.
}
return new int[] {-1, -1};
}
public static void main(String[] args) {
System.out.println(Arrays.toString(pairWithSum(new int[] {2, 7, 11, 15}, 9))); // [0, 1]
System.out.println(Arrays.toString(pairWithSum(new int[] {1, 3, 4, 6, 8, 11}, 10))); // [2, 3]
System.out.println(Arrays.toString(pairWithSum(new int[] {1, 2, 3}, 7))); // [-1, -1]
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
You're given an array height where height[i] is a vertical line at position i. Pick two lines that, together with the x-axis, hold the most water. Return that amount.
Example: [1, 8, 6, 2, 5, 4, 8, 3, 7] returns 49, from the lines at index 1 (height 8) and index 8 (height 7): min(8, 7) * (8 - 1) = 49.
Show SolutionHide Solution
Approach
The water between two lines is min(height[left], height[right]) * (right - left). Start with the widest container and work inward, keeping the best area you've seen.
Which pointer moves? The shorter line sets the water level. Moving the taller one inward makes the container narrower and can't raise the level, because the short line still caps it. So no pair that keeps the short line can beat what you just measured, and you can drop it.
Time O(n)Space O(1)
def max_area(height: list[int]) -> int:
"""Most water two of the vertical lines in `height` can hold."""
left, right = 0, len(height) - 1
best = 0
while left < right:
water = min(height[left], height[right]) * (right - left)
best = max(best, water)
# The shorter line caps the water. Moving the taller one inward can only
# shrink the width without raising that cap, so move the shorter one.
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
if __name__ == "__main__":
print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 49
print(max_area([1, 1])) # 1
print(max_area([4, 3, 2, 1, 4])) # 16// Most water two of the vertical lines in `height` can hold.
export function maxArea(height: number[]): number {
let left = 0;
let right = height.length - 1;
let best = 0;
while (left < right) {
const water = Math.min(height[left], height[right]) * (right - left);
best = Math.max(best, water);
// The shorter line caps the water. Moving the taller one inward can only
// shrink the width without raising that cap, so move the shorter one.
if (height[left] < height[right]) left++;
else right--;
}
return best;
}
console.log(maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7])); // 49
console.log(maxArea([1, 1])); // 1
console.log(maxArea([4, 3, 2, 1, 4])); // 16package main
import "fmt"
// maxArea returns the most water two of the vertical lines in height can hold.
func maxArea(height []int) int {
left, right := 0, len(height)-1
best := 0
for left < right {
water := min(height[left], height[right]) * (right - left)
best = max(best, water)
// The shorter line caps the water. Moving the taller one inward can only
// shrink the width without raising that cap, so move the shorter one.
if height[left] < height[right] {
left++
} else {
right--
}
}
return best
}
func main() {
fmt.Println(maxArea([]int{1, 8, 6, 2, 5, 4, 8, 3, 7})) // 49
fmt.Println(maxArea([]int{1, 1})) // 1
fmt.Println(maxArea([]int{4, 3, 2, 1, 4})) // 16
}using System;
public class Solution
{
// Most water two of the vertical lines in height can hold.
public static int MaxArea(int[] height)
{
int left = 0, right = height.Length - 1;
int best = 0;
while (left < right)
{
int water = Math.Min(height[left], height[right]) * (right - left);
best = Math.Max(best, water);
// The shorter line caps the water. Moving the taller one inward can only
// shrink the width without raising that cap, so move the shorter one.
if (height[left] < height[right]) left++;
else right--;
}
return best;
}
public static void Main()
{
Console.WriteLine(MaxArea(new[] { 1, 8, 6, 2, 5, 4, 8, 3, 7 })); // 49
Console.WriteLine(MaxArea(new[] { 1, 1 })); // 1
Console.WriteLine(MaxArea(new[] { 4, 3, 2, 1, 4 })); // 16
}
}public class Solution {
// Most water two of the vertical lines in height can hold.
static int maxArea(int[] height) {
int left = 0, right = height.length - 1;
int best = 0;
while (left < right) {
int water = Math.min(height[left], height[right]) * (right - left);
best = Math.max(best, water);
// The shorter line caps the water. Moving the taller one inward can only
// shrink the width without raising that cap, so move the shorter one.
if (height[left] < height[right]) left++;
else right--;
}
return best;
}
public static void main(String[] args) {
System.out.println(maxArea(new int[] {1, 8, 6, 2, 5, 4, 8, 3, 7})); // 49
System.out.println(maxArea(new int[] {1, 1})); // 1
System.out.println(maxArea(new int[] {4, 3, 2, 1, 4})); // 16
}
}Problem
Return every unique triplet [a, b, c] from nums where a + b + c = 0. The same triplet shouldn't appear twice, even if the input has duplicate values.
Example: [-1, 0, 1, 2, -1, -4] returns [[-1, -1, 2], [-1, 0, 1]].
Show SolutionHide Solution
Approach
Sort the array, then fix each number in turn as the first element of the triplet. What's left is the pair-sum template on the rest of the array, looking for -nums[i].
- Skip a first element that equals the one before it. It would find exactly the same triplets.
- After a match, move both pointers and keep moving them past equal values for the same reason.
- Once the fixed number is positive, stop. Three numbers that are all positive can't sum to zero.
Sorting is O(n log n), and the n scans of O(n) each dominate at O(n²). No known approach is meaningfully faster, and it beats the O(n³) of checking every triplet. The only extra memory is the sorted copy, which leaves the caller's array untouched. Sort in place instead if you're allowed to reorder the input.
Time O(n²)Space O(n) for the sorted copy
def three_sum(nums: list[int]) -> list[list[int]]:
"""Every unique triplet in `nums` that sums to zero, each in ascending order."""
nums = sorted(nums)
result = []
for i in range(len(nums) - 2):
if nums[i] > 0:
break # Everything after is positive too.
if i > 0 and nums[i] == nums[i - 1]:
continue # Same first number, same triplets.
left, right = i + 1, len(nums) - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
left += 1
elif total > 0:
right -= 1
else:
result.append([nums[i], nums[left], nums[right]])
left += 1
right -= 1
# Step past repeats so the same triplet isn't added twice.
while left < right and nums[left] == nums[left - 1]:
left += 1
while left < right and nums[right] == nums[right + 1]:
right -= 1
return result
if __name__ == "__main__":
print(three_sum([-1, 0, 1, 2, -1, -4])) # [[-1, -1, 2], [-1, 0, 1]]
print(three_sum([0, 1, 1])) # []
print(three_sum([0, 0, 0, 0])) # [[0, 0, 0]]// Every unique triplet in `nums` that sums to zero, each in ascending order.
export function threeSum(nums: number[]): number[][] {
const sorted = [...nums].sort((a, b) => a - b);
const result: number[][] = [];
for (let i = 0; i < sorted.length - 2; i++) {
if (sorted[i] > 0) break; // Everything after is positive too.
if (i > 0 && sorted[i] === sorted[i - 1]) continue; // Same first number, same triplets.
let left = i + 1;
let right = sorted.length - 1;
while (left < right) {
const sum = sorted[i] + sorted[left] + sorted[right];
if (sum < 0) left++;
else if (sum > 0) right--;
else {
result.push([sorted[i], sorted[left], sorted[right]]);
left++;
right--;
// Step past repeats so the same triplet isn't added twice.
while (left < right && sorted[left] === sorted[left - 1]) left++;
while (left < right && sorted[right] === sorted[right + 1]) right--;
}
}
}
return result;
}
console.log(JSON.stringify(threeSum([-1, 0, 1, 2, -1, -4]))); // [[-1,-1,2],[-1,0,1]]
console.log(JSON.stringify(threeSum([0, 1, 1]))); // []
console.log(JSON.stringify(threeSum([0, 0, 0, 0]))); // [[0,0,0]]package main
import (
"fmt"
"slices"
)
// threeSum returns every unique triplet in nums that sums to zero, each in
// ascending order.
func threeSum(nums []int) [][]int {
sorted := slices.Clone(nums)
slices.Sort(sorted)
result := [][]int{}
for i := 0; i < len(sorted)-2; i++ {
if sorted[i] > 0 {
break // Everything after is positive too.
}
if i > 0 && sorted[i] == sorted[i-1] {
continue // Same first number, same triplets.
}
left, right := i+1, len(sorted)-1
for left < right {
sum := sorted[i] + sorted[left] + sorted[right]
switch {
case sum < 0:
left++
case sum > 0:
right--
default:
result = append(result, []int{sorted[i], sorted[left], sorted[right]})
left++
right--
// Step past repeats so the same triplet isn't added twice.
for left < right && sorted[left] == sorted[left-1] {
left++
}
for left < right && sorted[right] == sorted[right+1] {
right--
}
}
}
}
return result
}
func main() {
fmt.Println(threeSum([]int{-1, 0, 1, 2, -1, -4})) // [[-1 -1 2] [-1 0 1]]
fmt.Println(threeSum([]int{0, 1, 1})) // []
fmt.Println(threeSum([]int{0, 0, 0, 0})) // [[0 0 0]]
}using System;
using System.Collections.Generic;
using System.Linq;
public class Solution
{
// Every unique triplet in nums that sums to zero, each in ascending order.
public static List<int[]> ThreeSum(int[] nums)
{
var sorted = nums.OrderBy(n => n).ToArray();
var result = new List<int[]>();
for (int i = 0; i < sorted.Length - 2; i++)
{
if (sorted[i] > 0) break; // Everything after is positive too.
if (i > 0 && sorted[i] == sorted[i - 1]) continue; // Same first number, same triplets.
int left = i + 1, right = sorted.Length - 1;
while (left < right)
{
int sum = sorted[i] + sorted[left] + sorted[right];
if (sum < 0) left++;
else if (sum > 0) right--;
else
{
result.Add(new[] { sorted[i], sorted[left], sorted[right] });
left++;
right--;
// Step past repeats so the same triplet isn't added twice.
while (left < right && sorted[left] == sorted[left - 1]) left++;
while (left < right && sorted[right] == sorted[right + 1]) right--;
}
}
}
return result;
}
public static void Main()
{
Console.WriteLine(Format(ThreeSum(new[] { -1, 0, 1, 2, -1, -4 }))); // [[-1, -1, 2], [-1, 0, 1]]
Console.WriteLine(Format(ThreeSum(new[] { 0, 1, 1 }))); // []
Console.WriteLine(Format(ThreeSum(new[] { 0, 0, 0, 0 }))); // [[0, 0, 0]]
}
static string Format(List<int[]> triplets) =>
"[" + string.Join(", ", triplets.Select(t => "[" + string.Join(", ", t) + "]")) + "]";
}import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// Every unique triplet in nums that sums to zero, each in ascending order.
static List<List<Integer>> threeSum(int[] nums) {
int[] sorted = nums.clone();
Arrays.sort(sorted);
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < sorted.length - 2; i++) {
if (sorted[i] > 0) break; // Everything after is positive too.
if (i > 0 && sorted[i] == sorted[i - 1]) continue; // Same first number, same triplets.
int left = i + 1, right = sorted.length - 1;
while (left < right) {
int sum = sorted[i] + sorted[left] + sorted[right];
if (sum < 0) left++;
else if (sum > 0) right--;
else {
result.add(List.of(sorted[i], sorted[left], sorted[right]));
left++;
right--;
// Step past repeats so the same triplet isn't added twice.
while (left < right && sorted[left] == sorted[left - 1]) left++;
while (left < right && sorted[right] == sorted[right + 1]) right--;
}
}
}
return result;
}
public static void main(String[] args) {
System.out.println(threeSum(new int[] {-1, 0, 1, 2, -1, -4})); // [[-1, -1, 2], [-1, 0, 1]]
System.out.println(threeSum(new int[] {0, 1, 1})); // []
System.out.println(threeSum(new int[] {0, 0, 0, 0})); // [[0, 0, 0]]
}
}Common Pitfalls
- Using opposite-ends pointers on unsorted input. Without order, a sum that's too small doesn't tell you which pointer to move.
- Looping while
left <= rightin a pair search, which pairs an element with itself. - Forgetting to skip duplicates in 3Sum and returning the same triplet more than once.
- Sorting when the question asks for original indices. Sorting moves the values, so track the indices alongside them or use a hash map instead.
Practice
More problems to try on your own.