leetcode / Two pointers / Two pointers intro

Two pointers

Two indices walk an array, usually from opposite ends, and every move throws away part of the search space for good. This page explains the argument that makes the discard safe, which is what separates a pattern you understand from a trick you memorised, and applies it to pair sums, containers, triples and palindromes.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The invariant
  5. The same proof for containers
  6. Reducing to pairs
  7. Worked example
  8. Implementation
  9. Variations and edge cases
  10. Complexity and tradeoffs
  11. Problems
  12. Easy
  13. Medium
  14. Hard
  15. Also uses this pattern
  16. Connections
  17. Read first
  18. Read next
  19. Check yourself
  20. References

Problem

Brute force on "find a pair" or "best pair of positions" checks all O(n²) pairs. When the input has order (sorted values, or a quantity that depends on the distance between positions), you can often prove that a whole row of pairs cannot contain the answer, and skip it. Two pointers are the bookkeeping for that proof.

SignalPatternWhy
Sorted array, find a pair with a given sumConverging pointersToo small moves the left end up, too large moves the right end down.
"Three sum", "four sum", "closest sum"Sort, fix one, converge on the restEach fixed element reduces the problem to the pair search.
Palindrome check, reverse in place, "almost a palindrome"Pointers from both ends moving inwardThe ends must match, and what is inside is the same question, smaller.
Maximise width times a limiting heightMove the limiting sideMoving the taller side can only shrink width without raising the limit.
Merge two sorted arrays, or fill from the larger endOne pointer per inputEach pointer marks the smallest (or largest) value not yet used.
Partition into groups (Dutch flag)Low, mid, high pointersThree zones are maintained while one pointer scans.

Intuition

Imagine looking for two numbers that add to 14 in a sorted row, with a finger on each end. If the two numbers are too big, the right-hand finger's number is too big for every partner, because the left finger holds the smallest partner it could have. So that number can be ruled out entirely and the finger steps inward. The symmetric argument discards the left number when the sum is too small. Each step eliminates a whole row or column of the pair table, so n steps cover n² pairs.

Converging pointers on a sorted array, target 14 The sorted array 1, 3, 4, 6, 8, 11, 15 drawn once per step. Step 1: left at index 0, right at index 6, sum 16, action move right.Step 2: left at index 0, right at index 5, sum 12, action move left.Step 3: left at index 1, right at index 5, sum 14, action found. The pointers meet the target at indices 1 and 5. step 1 1 3 4 6 8 11 15 1 + 15 = 16: move right step 2 1 3 4 6 8 11 15 1 + 11 = 12: move left step 3 1 3 4 6 8 11 15 3 + 11 = 14: found

Amber cells are the pointers; blue cells are ruled out and never looked at again.

Mechanics

The invariant

For converging pointers left < right, the invariant is: if a solution exists, it lies within [left, right]. Each move must preserve that. In the sorted pair sum, when nums[left] + nums[right] > target, the value at right plus anything at or after left is at least as large, so right belongs to no solution and can be dropped. The mirrored argument covers "too small". The loop ends when the pointers meet, with the window empty, which proves there was no solution.

The same proof for containers

Water held between two walls is min(h[left], h[right]) × (right − left). Say the left wall is the shorter. Any container using this wall and a position inside the window has smaller width and height still capped by this wall, so it cannot beat the current one. The left wall is finished: step it inward. Moving the taller wall instead could only lose width without lifting the cap, so that choice is never needed. This is an exchange argument, and it is what you say aloud in an interview.

Reducing to pairs

For three sum, sort and fix the smallest element at index i. The rest is the converging pair search for the target −nums[i] on the suffix after i. Duplicates are handled by skipping a fixed value equal to the previous fixed value, and by stepping the left pointer past repeats after a hit. Sorting costs O(n log n) and the n pair searches cost O(n²), so the total is O(n²), against O(n³) for brute force.

Container areas as the pointers move inward A bar chart of heights 1, 8, 6, 2, 5, 4, 8, 3, 7. The two-pointer scan evaluates 8 containers with areas 8, 49, 18, 40, 24, 6, 10, 4; the best is 49, between indices 1 and 8. 1 8 6 2 5 4 8 3 7 best container: min height × width = 49; the scan looked at only 8 of 36 pairs.

Green walls are the best pair. Every other pair was discarded by the move rule without being evaluated.

Worked example

Pair sum 14 in [1, 3, 4, 6, 8, 11, 15]: 3 comparisons instead of 21 pairs.

LeftRightSumAction
0 (1)6 (15)16move right
0 (1)5 (11)12move left
1 (3)5 (11)14found

Container with heights [1, 8, 6, 2, 5, 4, 8, 3, 7]:

LeftRightAreaBest so farMove
0888left
184949right
171849right
164049left
262449left
36649left
461049left
56449left

The answer is 49. In the first step the left wall (height 1) is the shorter, so it moves, exactly as the proof says: it can never be part of anything larger.

Three sum on [-1, 0, 1, 2, -1, -4, -1] sorts to [-4, -1, -1, -1, 0, 1, 2] and finds the triples [-1, -1, 2], [-1, 0, 1], after skipping the repeated −1 values as fixed elements.

Implementation

def pair_sum_sorted(nums, target):                  # indices, or None
    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
        else:
            right -= 1
    return None


def max_area(heights):
    left, right, best = 0, len(heights) - 1, 0
    while left < right:
        best = max(best, min(heights[left], heights[right]) * (right - left))
        if heights[left] <= heights[right]:
            left += 1
        else:
            right -= 1
    return best


def three_sum(nums):
    nums.sort()
    out = []
    for i in range(len(nums) - 2):
        if i and nums[i] == nums[i - 1]:
            continue
        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:
                out.append([nums[i], nums[left], nums[right]])
                left += 1
                while left < right and nums[left] == nums[left - 1]:
                    left += 1
    return out


def is_palindrome(text):
    left, right = 0, len(text) - 1
    while left < right:
        if not text[left].isalnum():
            left += 1
        elif not text[right].isalnum():
            right -= 1
        elif text[left].lower() != text[right].lower():
            return False
        else:
            left += 1
            right -= 1
    return True

Variations and edge cases

  • Same-direction pointers. The read and write indices from the previous page are two pointers moving the same way. Merge two sorted lists uses one pointer per list.
  • Almost-palindrome. On the first mismatch, try skipping the left character or the right one and check the rest with the plain palindrome test; at most one skip is allowed.
  • Closest sum. Same converging loop, but track the sum nearest to the target instead of stopping at equality.
  • Four sum and k-sum. Add outer loops (each with duplicate skipping) around the pair search; the cost is O(n^(k−1)).
  • Greedy pairing. Boats and similar problems: sort, then pair the heaviest remaining with the lightest when they fit, otherwise take the heaviest alone.
  • Three-way partition. Keep low, mid, high; swap 0s behind low and 2s ahead of high. Do not advance mid after swapping with high, since the value that arrived is unexamined.
  • Edge cases: fewer than two (or three) elements, all equal values with duplicate skipping, using < versus <= in the loop condition (a pointer must not pair with itself), unsorted input (sort first, and remember the original indices if the answer asks for them), and overflow in other languages.

Complexity and tradeoffs

  • Time: O(n) for a single converging pass; O(n log n) if sorting is needed first; O(n²) for three sum.
  • Space: O(1) beyond the sort (and the output).
  • Baselines. Brute force is O(n²) for pairs. A hash map solves unsorted pair sum in O(n) time and O(n) space and also preserves indices; choose the hash map when the array is unsorted and the answer needs original positions.
  • When it fails. If neither pointer move can be justified by a monotonic argument, the pattern does not apply. Negative numbers break window-style pointers that move in the same direction (see prefix sums), but converging pointers on a sorted array are fine with negatives.

Problems

Home problems range from reversing a string through sorted pair searches to the container and rain-water problems, whose correctness rests on the discard argument above.

Easy

  • 26. Remove Duplicates from Sorted Array NeetCode 250
    A slow writer pointer keeps the unique prefix while a fast reader skips anything equal to the last value written.
  • 88. Merge Sorted Array NeetCode 250
    Fill the first array from the back, taking the larger tail each time, so nothing is overwritten before it is read.
  • 125. Valid Palindrome Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Move two pointers inward, skipping non-alphanumeric characters, and compare lowercase letters until they meet.
  • 283. Move Zeroes Grind 169
    Move each non-zero element forward to a write pointer, then the slots after it are all zeros.
  • 344. Reverse String NeetCode 250
    Swap the two ends and move both pointers inward until they meet.
  • 680. Valid Palindrome II NeetCode 250
    Move inward until the first mismatch, then check whether skipping either of the two characters leaves a palindrome.
  • 844. Backspace String Compare Grind 169
    Scan both strings from the end, skipping characters erased by backspaces, and compare the next surviving characters.
  • 977. Squares of a Sorted Array Grind 169
    The largest square is at one of the two ends, so fill the result from the back with the larger end's square.
  • 1768. Merge Strings Alternately NeetCode 250
    Take one character from each string in turn, then append whatever is left of the longer string.

Medium

  • 11. Container With Most Water Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Start with the widest container and always move the shorter wall inward, since keeping it can never produce a larger area.
  • 15. 3Sum Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Sort, fix the smallest element, and run a converging pair search on the rest, skipping equal neighbours to avoid duplicate triples.
  • 16. 3Sum Closest Grind 169
    After sorting, fix one element and converge a pair on the rest, keeping whichever sum has landed closest to the target.
  • 18. 4Sum NeetCode 250
    Fix two outer elements in nested loops over the sorted array, then let a converging pair finish the sum, skipping duplicates at every level.
  • 75. Sort Colors Grind 169 NeetCode 250
    Partition into three zones with low, mid and high pointers, swapping 0s down and 2s up while mid scans.
  • 167. Two Sum II - Input Array Is Sorted NeetCode 150 NeetCode 250
    With the array sorted, a sum that is too small moves the left pointer right and a sum that is too large moves the right pointer left.
  • 881. Boats to Save People NeetCode 250
    Sort, then pair the heaviest remaining person with the lightest whenever they fit together, otherwise send the heaviest alone.

Hard

  • 42. Trapping Rain Water Grind 169 NeetCode 150 NeetCode 250
    Water above a bar is set by the smaller of the tallest walls on its left and right, so move inward from whichever side's tallest wall is lower.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Sliding windows are two pointers that move in the same direction, and fast and slow pointers on linked lists use the same machinery with different movement rules. Binary search is the extreme case of discarding half the range per step. Greedy exchange arguments, like the one for containers, appear again on the greedy page.

Check yourself

In the pair-sum scan, why can the right pointer be dropped when the sum is too large?

The left value is the smallest partner remaining, so if it already overshoots, every other partner overshoots too. On the example, 1 + 15 = 16 is above 14, so 15 is in no pair.

Why does container most water move the shorter wall?

Any container using the shorter wall and a position inside the window is both narrower and still capped by that wall, so it cannot be larger. The scan evaluated 8 containers and found 49.

Why does three sum skip a fixed element equal to the previous one?

Every triple that starts with the earlier copy was already found, including those using the later copy as a middle element, so repeating the fixed value only produces duplicates. The example has three −1 values and still returns 2 distinct triples.

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms: the merge step of merge sort (section 2.3) and the discussion of loop invariants.
  • Sedgewick and Wayne, Algorithms: section 2.2 on merging and section 2.3 on partitioning.
  • Steven Skiena, The Algorithm Design Manual: the section on sorting and searching applications, including pair and closest-pair searches.
  • Edsger Dijkstra, "Dutch national flag" in A Discipline of Programming (1976), the origin of the three-way partition.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.