Skip to content
SparkTime, home

Lesson 14 · Core Algorithms and Reusable Patterns

Two Pointers: Solve Problems by Coordinating Indices

Replace nested loops with two indices that move in a controlled way. Learn the opposite-ends and same-direction patterns, why each pointer move is safe, and solve pair sum, container with most water, 3Sum and in-place deduplication.

On this page

Key takeaways

  • Two pointers replace a nested loop by moving two indices through the data, so each element is visited a constant number of times.
  • Opposite-end pointers start at both ends of sorted data and move inwards. Same-direction pointers have a reader that scans and a writer that records results.
  • Every pointer move must be justified: it discards candidates that cannot be part of any answer, while keeping an invariant true.
  • Pair sum on sorted data, container with most water, 3Sum, removing duplicates in place and trapping rain water are all two-pointer problems.
  • The technique usually needs sorted data or a monotonic property. If the data is unsorted and you must keep original positions, a hash map may fit better.

Imagine two friends searching a long corridor of numbered rooms for a pair of rooms whose numbers add up to a target. One starts at the front and the other at the back. Whenever the total is too low, the person at the front steps forward to a bigger room number. When it is too high, the person at the back steps in to a smaller one. They meet in the middle having checked very few pairs, and they never needed to try every combination.

That picture is the two pointers technique. You use two indices instead of one, and you move them cleverly so that a problem that looks like it needs a loop inside a loop is solved in a single pass. It is one of the most useful patterns in interviews, and you have already seen it twice: reversing an array, and checking a palindrome from both ends.

What you will learn

#
  • The two main shapes of the technique: pointers from opposite ends, and pointers in the same direction.
  • How to explain why a pointer move is safe, which is what interviewers really ask.
  • How to solve pair sum, container with most water, 3Sum, deduplication and trapping rain water.
  • How to tell when two pointers do not apply.

The idea in one sentence

#

Keep two indices, and at every step move one of them in a way that provably discards at least one candidate without losing any answer. If every step shrinks the search space by one candidate, the whole process takes O(n) steps, not O(n^2).

Pattern A: pointers from opposite ends

#

Start with left = 0 and right = n - 1. Compare something about a[left] and a[right], and move one pointer inwards. Stop when they meet. This usually needs sorted data, or a property that makes the choice of which pointer to move clear.

Problem: pair sum in a sorted array

#

Problem. Given a sorted array and a target, find two different positions whose values add up to the target.

Brute force. Try every pair: O(n^2).

Two pointers. Look at sum = a[left] + a[right]:

  • if sum == target, we found the pair;
  • if sum < target, we need a bigger sum, so move left right (towards bigger values);
  • if sum > target, we need a smaller sum, so move right left.

Why is the move safe? Suppose sum < target. Every other candidate partner for a[left] lies in a[left + 1 .. right - 1], and those values are all <= a[right] because the array is sorted. So a[left] + a[j] <= a[left] + a[right] < target for every j: a[left] cannot pair with anything left in the window. We lose nothing by discarding it. The case sum > target is the mirror image: a[right] is too big to pair with anything, so discard it.

This argument is the invariant of the algorithm: if any valid pair exists, it lies inside a[left..right]. It is true at the start, and each step preserves it. When the pointers meet (left == right), the window holds a single element, no valid pair remains, and we report "not found".

We use left < right (not <=) because the two positions must be different.

Problem: container with most water

#

Problem. The array height describes vertical lines at positions 0, 1, ..., n - 1. Choose two lines that, with the x-axis, hold the most water. The water level is limited by the shorter line, so the area for lines at left and right is min(height[left], height[right]) × (right - left).

Brute force. Try every pair: O(n^2).

Two pointers. Start with the widest container (left = 0, right = n - 1), record the area, and then move the pointer at the shorter line inwards.

Why the shorter one? Suppose height[left] < height[right]. Any container that keeps left and uses a smaller right' has a smaller width, and its height is still at most height[left] (the shorter line limits it). So it cannot beat the area we just computed, and left can never be part of a better container. We can discard left. Moving the taller line would be pointless, since the container's height cannot go above the shorter line while the width gets smaller.

Problem: 3Sum

#

Problem. Given an array of integers, find all unique triplets (a, b, c) with a + b + c = 0.

Brute force. Three nested loops: O(n^3).

Better. Sort the array. Fix the first number a[i]. Then you need two numbers in the rest of the array (positions after i) that sum to -a[i]. That is pair sum on a sorted range, solved with two pointers in O(n). Total: O(n^2).

To keep triplets unique, skip repeated values: skip i if a[i] == a[i - 1], and after finding a triplet, move both pointers past duplicate values. If a[i] > 0, no triplet starting at i can sum to zero (all later values are at least as large), so we can stop.

Java
// File: Main.javaimport java.util.ArrayList;import java.util.Arrays;import java.util.List;public class Main {    // Sorted input. Returns {left, right} or an empty array.    static int[] pairSumSorted(int[] a, int target) {        int left = 0;        int right = a.length - 1;        while (left < right) {            long sum = (long) a[left] + a[right];     // long: the sum of two ints can overflow int            if (sum == target) {                return new int[] {left, right};            }            if (sum < target) {                left++;            } else {                right--;            }        }        return new int[0];    }    static long maxWater(int[] height) {        int left = 0;        int right = height.length - 1;        long best = 0;        while (left < right) {            long area = (long) Math.min(height[left], height[right]) * (right - left);            best = Math.max(best, area);            if (height[left] < height[right]) {                left++;                               // the shorter line limits every container using it            } else {                right--;            }        }        return best;    }    static List<List<Integer>> threeSum(int[] nums) {        int[] a = nums.clone();        Arrays.sort(a);        List<List<Integer>> result = new ArrayList<>();        for (int i = 0; i < a.length - 2; i++) {            if (a[i] > 0) {                break;                                // the smallest is positive: no zero sum possible            }            if (i > 0 && a[i] == a[i - 1]) {                continue;                             // skip a repeated first value            }            int left = i + 1;            int right = a.length - 1;            while (left < right) {                long sum = (long) a[i] + a[left] + a[right];                if (sum == 0) {                    result.add(Arrays.asList(a[i], a[left], a[right]));                    left++;                    right--;                    while (left < right && a[left] == a[left - 1]) {                        left++;                       // skip repeated second values                    }                    while (left < right && a[right] == a[right + 1]) {                        right--;                      // skip repeated third values                    }                } else if (sum < 0) {                    left++;                } else {                    right--;                }            }        }        return result;    }    static String show(List<List<Integer>> triplets) {        StringBuilder sb = new StringBuilder();        for (List<Integer> t : triplets) {            sb.append('(').append(t.get(0)).append(',').append(t.get(1)).append(',').append(t.get(2)).append(") ");        }        return triplets.size() + " triplets: " + sb.toString().trim();    }    public static void main(String[] args) {        System.out.println(Arrays.toString(pairSumSorted(new int[] {1, 2, 4, 7, 11, 15}, 15)));        System.out.println(Arrays.toString(pairSumSorted(new int[] {2, 5}, 4)));        System.out.println(Arrays.toString(pairSumSorted(new int[] {-3, -1, 0, 2, 5}, 2)));        System.out.println(Arrays.toString(pairSumSorted(new int[] {1, 1}, 2)));        System.out.println(Arrays.toString(pairSumSorted(new int[] {}, 3)));        System.out.println(maxWater(new int[] {1, 8, 6, 2, 5, 4, 8, 3, 7}) + " " + maxWater(new int[] {1, 1}) + " " + maxWater(new int[] {4, 3, 2, 1, 4}) + " " + maxWater(new int[] {5}));        System.out.println(show(threeSum(new int[] {-1, 0, 1, 2, -1, -4})));        System.out.println(show(threeSum(new int[] {0, 0, 0, 0})));        System.out.println(show(threeSum(new int[] {1, 2, 3})));    }}
Code
[2, 4][][0, 4][0, 1][]49 1 16 02 triplets: (-1,-1,2) (-1,0,1)1 triplets: (0,0,0)0 triplets:

Dry run of pairSumSorted([1, 2, 4, 7, 11, 15], 15):

left

right

a[left]

a[right]

Sum

Action

0

5

1

15

16

too big, right = 4

0

4

1

11

12

too small, left = 1

1

4

2

11

13

too small, left = 2

2

4

4

11

15

found: indexes 2 and 4

Four steps for six elements instead of up to 15 pairs.

Dry run of maxWater([1, 8, 6, 2, 5, 4, 8, 3, 7]):

left

right

Heights

Width

Area

Best

Move

0

8

1, 7

8

8

8

left (1 is shorter)

1

8

8, 7

7

49

49

right (7 is shorter)

1

7

8, 3

6

18

49

right

1

6

8, 8

5

40

49

right (equal, either is fine)

1

5

8, 4

4

16

49

right

1

4

8, 5

3

15

49

right

1

3

8, 2

2

4

49

right

1

2

8, 6

1

6

49

right, then the pointers meet

The best area is 49, found at the second step, and the remaining steps could not beat it.

Dry run of threeSum on [-1, 0, 1, 2, -1, -4]: sorted it is [-4, -1, -1, 0, 1, 2].

i

a[i]

Two pointers over the rest

Result

0

-4

needs two numbers summing to 4: best is 1 + 2 = 3, too small

none

1

-1

left at -1, right at 2: sum 0, a triplet (-1, -1, 2). Then left = 0, right = 1: sum 0, a triplet (-1, 0, 1)

two triplets

2

-1

same as the previous value, skipped

none

3

0

0 is not above zero, but the only pair left is (1, 2) with sum 3

none

4

1

not reached: the loop ends when fewer than three elements remain

Complexity: pair sum and container are O(n) time and O(1) extra space. 3Sum is O(n^2) (sorting is O(n log n), and then n iterations each with an O(n) two-pointer pass), with O(1) or O(log n) extra space for the sort, not counting the output.

Pattern B: pointers in the same direction (reader and writer)

#

Here both pointers start at the left and move right, at different speeds or for different roles.

  • The reader scans every element.
  • The writer marks where the next kept element should be placed.

Everything to the left of the writer is the finished output. This pattern does in-place filtering with O(1) extra space.

Problem: remove duplicates from a sorted array

#

Problem. Remove duplicates in place from a sorted array, so each value appears once. Return the number of unique values k; the first k elements of the array must hold them in order. What is beyond position k does not matter.

Because the array is sorted, duplicates sit together. The writer position write is where the next new value goes. The reader looks at each element: if it differs from the last value we kept (a[write - 1]), it is new, so copy it to a[write] and advance the writer.

Java
// File: Main.javaimport java.util.Arrays;public class Main {    // Returns the number of distinct values; the first that many positions hold them.    static int removeDuplicates(int[] a) {        if (a.length == 0) {            return 0;        }        int write = 1;                               // a[0] is always kept        for (int read = 1; read < a.length; read++) {            if (a[read] != a[write - 1]) {           // differs from the last kept value                a[write] = a[read];                write++;            }        }        return write;    }    public static void main(String[] args) {        int[] a = {0, 0, 1, 1, 1, 2, 2, 3, 3, 4};        int k = removeDuplicates(a);        System.out.println(k + " " + Arrays.toString(Arrays.copyOf(a, k)));        int[] single = {7};        System.out.println(removeDuplicates(single) + " " + removeDuplicates(new int[] {}) + " " + removeDuplicates(new int[] {2, 2, 2}));    }}
Code
5 [0, 1, 2, 3, 4]1 0 1

Dry run on [0, 0, 1, 1, 1, 2]:

read

a[read]

Last kept a[write - 1]

New?

write after

Array (kept part first)

1

0

0

no

1

[0, 0, 1, 1, 1, 2]

2

1

0

yes, copy to index 1

2

[0, 1, 1, 1, 1, 2]

3

1

1

no

2

[0, 1, 1, 1, 1, 2]

4

1

1

no

2

[0, 1, 1, 1, 1, 2]

5

2

1

yes, copy to index 2

3

[0, 1, 2, 1, 1, 2]

The answer is 3, and the first three positions hold 0, 1, 2. The reader scans n elements and the writer never moves faster than the reader, so the time is O(n) and the space O(1). Moving zeroes to the end, from the arrays article, is the same pattern with a different "keep" rule.

Pattern C: pointers on two separate sequences

#

When you have two sorted sequences, use one pointer in each. You did exactly this when merging two sorted arrays, and when merging two sorted linked lists. The same idea gives the intersection of two sorted arrays, and the "is s a subsequence of t" check (advance the pointer in s only when the characters match).

Fast and slow pointers

#

In Linked Lists, the slow pointer moved one step and the fast pointer two steps. That is the same-direction pattern with different speeds, used for finding the middle and detecting cycles.

When two pointers does not apply

#
  • Unsorted data where original positions matter. If you need the indexes in the original array and the array is unsorted, sorting loses them. A HashMap (see Hashing) is usually the right tool.
  • No monotonic property. The pointer moves must be justified by "this candidate can never be part of an answer". If you cannot argue that, the technique is not valid for the problem.
  • Subarray problems with negative numbers. Pointer logic for sums assumes that adding elements always increases the total. With negatives, that fails. We study this in Sliding Window and Prefix Sums.

Complexity summary

#

Problem

Time

Extra space

Needs

Pair sum, sorted

O(n)

O(1)

Sorted input

Pair sum, unsorted (sort first)

O(n log n)

O(1) or O(log n)

Loses original indexes

Container with most water

O(n)

O(1)

Nothing, justified by the shorter-line argument

3Sum

O(n^2)

O(1) beyond sorting and output

Sorting

Remove duplicates, sorted, in place

O(n)

O(1)

Sorted input

Trapping rain water (exercise)

O(n)

O(1)

Nothing

Edge cases and common mistakes

#
  • Using left <= right when the two positions must differ. A value could pair with itself.
  • Moving the wrong pointer. Always justify the move with "this element can never be part of an answer".
  • Forgetting that sorted is required for the pair-sum pointer logic.
  • Duplicates in 3Sum. Skip repeated values for the first element and after each found triplet, or you will report the same triplet more than once.
  • Overflow. Sums of two or three ints can overflow. Use long for the sum.
  • Empty and one-element arrays. Loops like i < a.length - 2 handle short arrays safely, but check.
  • Reading past the end when you compare a[i] with a[i + 1] or a[right + 1]. Check the bounds first.
  • Forgetting to return the new length in in-place filtering, or assuming the elements beyond it are meaningful.
  • Applying the pattern to unsorted data without sorting, or sorting when the indexes were needed.

Exercises

#

Exercise 1

Trace

#

Run pairSumSorted on [-2, 0, 3, 5, 8, 12] with target 10. Write left, right, the sum and the decision at each step. What does it return? Then do target 100.

Exercise 2: Squares of a sorted array

#

Given a sorted array that may contain negative numbers, return an array of the squares of each number, sorted in increasing order. For [-4, -1, 0, 3, 10] the answer is [0, 1, 9, 16, 100]. Do it in O(n), without sorting the squares.

Exercise 3: Allow at most two copies

#

Remove duplicates in place from a sorted array so that each value appears at most twice. Return the new length. For [1, 1, 1, 2, 2, 3] the answer is 5, with the array starting [1, 1, 2, 2, 3].

Exercise 4: Debug

#

This method should decide whether a sorted array has two different elements adding up to the target. Find the problems and give an input that breaks each.

Java
static boolean hasPairBuggy(int[] a, int target) {    int left = 0;    int right = a.length - 1;    while (left <= right) {        int sum = a[left] + a[right];        if (sum == target) {            return true;        }        if (sum < target) {            right--;        } else {            left++;        }    }    return false;}

Exercise 5: Interview-style, trapping rain water

#

An array of non-negative integers represents the heights of bars, each of width 1. After rain, how much water is trapped between the bars? For [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] the answer is 6. Aim for O(n) time and O(1) extra space.

Show solutionHide solution

Target 10 on [-2, 0, 3, 5, 8, 12]:

left

right

Values

Sum

Decision

0

5

-2, 12

10

equal to the target: found

It returns [0, 5] immediately: -2 + 12 = 10.

Target 100:

left

right

Values

Sum

Decision

0

5

-2, 12

10

too small, left = 1

1

5

0, 12

12

too small, left = 2

2

5

3, 12

15

too small, left = 3

3

5

5, 12

17

too small, left = 4

4

5

8, 12

20

too small, left = 5

5

5

left == right: stop, no pair

It returns an empty result. Every step moved a pointer, and the loop ended after at most n - 1 moves.

Exercise 2

Show solutionHide solution

The largest square is at one of the two ends: the most negative number or the largest positive number has the largest absolute value. So fill the result from the back. Compare the absolute values at left and right; the larger one's square goes into the next free position at the end, and that pointer moves inwards.

Why is this safe? The values in a[left..right] have their largest absolute value at one of the ends, because the array is sorted. So the larger of the two end squares is the largest remaining square. We use long because a square of a large int does not fit in an int.

Java
// File: Main.javaimport java.util.Arrays;public class Main {    static long[] sortedSquares(int[] a) {        long[] result = new long[a.length];        int left = 0;        int right = a.length - 1;        for (int pos = a.length - 1; pos >= 0; pos--) {            long leftSquare = (long) a[left] * a[left];            long rightSquare = (long) a[right] * a[right];            if (leftSquare > rightSquare) {                result[pos] = leftSquare;                left++;            } else {                result[pos] = rightSquare;                right--;            }        }        return result;    }    public static void main(String[] args) {        System.out.println(Arrays.toString(sortedSquares(new int[] {-4, -1, 0, 3, 10})));        System.out.println(Arrays.toString(sortedSquares(new int[] {-7, -3, 2, 3, 11})));        System.out.println(Arrays.toString(sortedSquares(new int[] {-100000, 5})));        System.out.println(Arrays.toString(sortedSquares(new int[] {})));    }}
Code
[0, 1, 9, 16, 100][4, 9, 9, 49, 121][25, 10000000000][]

Time O(n), and extra space is the output array. Sorting the squares afterwards would be O(n log n), so the two pointers save the logarithm.

Exercise 3

Show solutionHide solution

Generalise the "same as the last kept value" rule. An element may be kept if fewer than two copies are already kept, which means it differs from the element two positions before the writer, a[write - 2]. Because the array is sorted, if a[read] == a[write - 2], then a[write - 1] is also equal, so two copies are already kept.

Java
// File: Main.javaimport java.util.Arrays;public class Main {    static int removeDuplicatesAtMostTwice(int[] a) {        int write = 0;        for (int read = 0; read < a.length; read++) {            if (write < 2 || a[read] != a[write - 2]) {                a[write] = a[read];                write++;            }        }        return write;    }    public static void main(String[] args) {        int[] a = {1, 1, 1, 2, 2, 3};        int k = removeDuplicatesAtMostTwice(a);        System.out.println(k + " " + Arrays.toString(Arrays.copyOf(a, k)));        int[] b = {0, 0, 1, 1, 1, 1, 2, 3, 3};        int m = removeDuplicatesAtMostTwice(b);        System.out.println(m + " " + Arrays.toString(Arrays.copyOf(b, m)));        System.out.println(removeDuplicatesAtMostTwice(new int[] {}) + " " + removeDuplicatesAtMostTwice(new int[] {5, 5, 5, 5}));    }}
Code
5 [1, 1, 2, 2, 3]7 [0, 0, 1, 1, 2, 3, 3]0 2

The first two elements are always kept (write < 2). Time O(n), space O(1). Replace 2 by any k to allow at most k copies.

Exercise 4

Show solutionHide solution

There are three problems.

  1. The pointer moves are reversed. When sum < target we need a larger sum, so we must move left forward, not right backward. The code does the opposite. For [1, 2, 4, 7, 11, 15] and target 15, the correct answer is true (4 + 11), but the buggy method returns false, because it keeps moving the wrong pointer.
  2. left <= right lets the same element pair with itself. For [2, 5] and target 4, with the moves fixed but this condition left as it is, the pointers reach left = right = 0 and report 2 + 2 = 4, which uses one element twice. The condition must be left < right.
  3. int sum can overflow. Two values near Integer.MAX_VALUE add up to a negative number. Use long.
Java
// File: Main.javapublic class Main {    static boolean hasPairBuggy(int[] a, int target) {        int left = 0;        int right = a.length - 1;        while (left <= right) {            int sum = a[left] + a[right];            if (sum == target) {                return true;            }            if (sum < target) {                right--;            } else {                left++;            }        }        return false;    }    // Moves fixed, but still using <= to show the second bug on its own.    static boolean hasPairSelfUse(int[] a, int target) {        int left = 0;        int right = a.length - 1;        while (left <= right) {            int sum = a[left] + a[right];            if (sum == target) {                return true;            }            if (sum < target) {                left++;            } else {                right--;            }        }        return false;    }    static boolean hasPairFixed(int[] a, int target) {        int left = 0;        int right = a.length - 1;        while (left < right) {            long sum = (long) a[left] + a[right];            if (sum == target) {                return true;            }            if (sum < target) {                left++;            } else {                right--;            }        }        return false;    }    public static void main(String[] args) {        int[] a = {1, 2, 4, 7, 11, 15};        System.out.println(hasPairBuggy(a, 15) + " " + hasPairFixed(a, 15));        int[] b = {2, 5};        System.out.println(hasPairSelfUse(b, 4) + " " + hasPairFixed(b, 4));    }}
Code
false truetrue false

Exercise 5

Show solutionHide solution

The water above a bar at position i is min(maxLeft, maxRight) - height[i], where maxLeft and maxRight are the tallest bars on its left and its right (counting the bar itself), and it is never negative.

The simple solution precomputes both maximums in two arrays (O(n) space). The two-pointer version avoids the arrays.

Keep left, right, and the tallest bar seen so far from each side, leftMax and rightMax. At each step, look at the shorter of height[left] and height[right]:

  • Suppose height[left] < height[right]. We know there is a wall at least as tall as height[right] somewhere on the right, which is taller than height[left]. So the right side will never be the limiting side for position left: its water level is decided by leftMax alone. Then the water above left is leftMax - height[left] (or zero if this bar is itself the new maximum). Process it and move left inwards.
  • Otherwise, do the mirror image on the right.

This processes every bar once. Time O(n), space O(1).

Java
// File: Main.javapublic class Main {    static int trap(int[] height) {        int left = 0;        int right = height.length - 1;        int leftMax = 0;        int rightMax = 0;        int water = 0;        while (left < right) {            if (height[left] < height[right]) {                if (height[left] >= leftMax) {                    leftMax = height[left];            // a new tallest wall on the left: no water here                } else {                    water += leftMax - height[left];                }                left++;            } else {                if (height[right] >= rightMax) {                    rightMax = height[right];                } else {                    water += rightMax - height[right];                }                right--;            }        }        return water;    }    public static void main(String[] args) {        System.out.println(trap(new int[] {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1}));        System.out.println(trap(new int[] {4, 2, 0, 3, 2, 5}));        System.out.println(trap(new int[] {}));        System.out.println(trap(new int[] {3}));        System.out.println(trap(new int[] {1, 2, 3}));    }}
Code
69000

For [4, 2, 0, 3, 2, 5]: the first and last bars (4 and 5) form the walls. The bar 2 traps 2 units, 0 traps 4, 3 traps 1, and 2 traps 2, but only up to the lower wall 4, so the total is 2 + 4 + 1 + 2 = 9. This is the answer printed.

Where to go next

#
  • Next: Sliding Window: Efficient Subarray and Substring Problems is the same-direction pattern applied to contiguous ranges, where two pointers mark the two ends of a window.
  • Related: Searching and Sorting explains the sorting step that many two-pointer solutions begin with, and Hashing is the alternative for unsorted pair-sum problems.
  • Practice habit: each time you move a pointer, say out loud, "I am discarding this element because ...". If you cannot finish the sentence, the move is a guess.
  • Guide · Technology

    How to Learn Data Structures and Algorithms Without Burning Out

  • Guide · Career

    The OA Landscape: Platforms, Formats, Proctoring and Rules

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages