Two Pointer

The two-pointer technique replaces a nested loop with two indices whose movement is driven by a simple rule. This page covers the three common shapes of the pattern, why discarding elements is safe, worked Python examples for each, and the problems where it does not apply.

The two-pointer technique walks two indices through a sequence — usually one at each end, or both moving forward at different speeds — to solve problems in O(n) that look like they need O(n²). The standard interview tool for sorted-array problems.

Complexity

Best Average Worst Space (Worst)
Ω(n) Θ(n) O(n) O(1)

How it works

Place pointers at both ends of a sorted array. Compare what they index; based on the comparison, move the left pointer right or the right pointer left, monotonically shrinking the search space. Each pointer moves at most n times — total O(n).

Python implementation

def two_sum_sorted(arr, target):
    """Find two indices in a SORTED array that sum to target. O(n)."""
    lo, hi = 0, len(arr) - 1
    while lo < hi:
        s = arr[lo] + arr[hi]
        if s == target:
            return (lo, hi)
        if s < target:
            lo += 1
        else:
            hi -= 1
    return None


def remove_duplicates_in_place(arr):
    """Remove duplicates from a sorted array in place. Returns new length."""
    if not arr: return 0
    write = 1
    for read in range(1, len(arr)):
        if arr[read] != arr[read - 1]:
            arr[write] = arr[read]
            write += 1
    return write


print(two_sum_sorted([1, 2, 4, 7, 11, 15], 13))   # (2, 4) — 4 + 11

a = [1, 1, 2, 3, 3, 4]
n = remove_duplicates_in_place(a)
print(a[:n])    # [1, 2, 3, 4]

Trade-offs

← Back to Algorithms


1. Opposite-end pointers

Opposite-end pointers: pair sum to 10 L=0, R=6: 1+11=12 > 10 1 2 4 5 7 9 11 R-- : 1+9=10 found 1 2 4 5 7 9 11 Sum too big: move R left. Sum too small: move L right. Each step discards one element.

On a sorted array, start one pointer at each end. If the current sum is larger than the target, every pair that uses the right element with any element at or to the right of L is also too large, so the right element can be discarded by moving R left. Symmetrically, a sum that is too small lets you discard the left element. Each step removes one candidate, so the loop runs at most n times. That argument is the whole correctness proof, and it relies entirely on the input being sorted.

2. Same-direction (slow/fast) pointers

Same-direction pointers: remove duplicates in place start: write=1 1 1 2 2 2 3 4 read=2 new value 1 2 2 2 2 3 4 read=5 new value 1 2 3 2 2 3 4 read=6 new value 1 2 3 4 2 3 4 Slow pointer marks the end of the kept prefix; fast pointer scans every element once.

In the second shape both pointers move forward. The fast (read) pointer visits every element; the slow (write) pointer marks where the next kept element goes. Everything left of the slow pointer is the answer so far. This gives in-place filtering, deduplication of sorted data, and partitioning in O(n) time and O(1) extra space. Floyd's cycle detection on linked lists is a variant where the fast pointer moves two steps per iteration; if there is a cycle the pointers must meet inside it.

3. Choosing a pattern

Choosing a two-pointer pattern Sorted array, pair/triple sums opposite ends O(n) after O(n log n) sort In-place filter or dedupe slow/fast same direction O(n), O(1) space Linked list cycle or middle fast moves 2x O(n), O(1) space

A rough decision rule: if the question asks about pairs or triples in sorted (or sortable) data, use opposite ends. If it asks you to modify an array in place while keeping order, use slow and fast. If it involves a linked list and you cannot index, use pointers moving at different speeds. When the problem talks about a contiguous range whose size changes, it is usually the sliding window pattern, which is a two-pointer special case.

4. Worked examples

def three_sum(nums):
    """All unique triplets summing to zero. O(n^2)."""
    nums.sort()
    res = []
    for i in range(len(nums) - 2):
        if i and nums[i] == nums[i - 1]:
            continue                       # skip duplicate anchors
        lo, hi = i + 1, len(nums) - 1
        while lo < hi:
            s = nums[i] + nums[lo] + nums[hi]
            if s < 0:
                lo += 1
            elif s > 0:
                hi -= 1
            else:
                res.append([nums[i], nums[lo], nums[hi]])
                while lo < hi and nums[lo] == nums[lo + 1]:
                    lo += 1
                lo, hi = lo + 1, hi - 1
    return res


def dedupe_sorted(a):
    """Remove duplicates in place, return new length."""
    if not a:
        return 0
    w = 1
    for r in range(1, len(a)):
        if a[r] != a[w - 1]:
            a[w] = a[r]
            w += 1
    return w


def is_palindrome(s):
    i, j = 0, len(s) - 1
    while i < j:
        if s[i] != s[j]:
            return False
        i, j = i + 1, j - 1
    return True


print(three_sum([-1, 0, 1, 2, -1, -4]))   # [[-1, -1, 2], [-1, 0, 1]]
print(dedupe_sorted([1, 1, 2, 2, 2, 3, 4]))  # 4
print(is_palindrome("racecar"))          # True

3Sum fixes one element and runs the opposite-end scan on the remainder, which turns an O(n^3) brute force into O(n^2). The duplicate-skipping lines are the part people most often get wrong: without them the output contains repeated triplets.

5. Trade-offs and when to use

ApproachTimeSpaceNeeds sorted input
Nested loopsO(n^2)O(1)No
Hash set lookupO(n)O(n)No
Sort + two pointersO(n log n)O(1) to O(n)Yes (sorts first)

For a single pair-sum query on unsorted data, a hash set is usually simpler and faster. Two pointers win when the data is already sorted, when memory is constrained, or when you need all pairs or triples without duplicates.

A useful habit when writing any two-pointer loop is to state the invariant in a comment before writing the body: what is known to be true about everything left of L and right of R, or about everything before the write pointer. If every branch of the loop body either preserves that invariant while moving at least one pointer, or returns, the loop is correct and terminates. Most bugs in interview solutions come from a branch that moves no pointer at all.

6. Pitfalls