Home / Algorithms

Two Pointers

Two indices moving with purpose turn an O(n²) nested loop into a single O(n) pass. The trick only works when the data is ordered — and that ordering is what tells each pointer which way to move.

Controls

target sum16

Pointers in Motion

step 0

Insight

Two pointers work when moving one of them reliably changes the answer in a known direction. On a sorted array, moving left rightwards can only increase the sum.

comparisons0
brute force0
best so far–

Complexity

Two pointers O(n)
Brute force O(n²)
Space O(1)

Overview

The two pointers technique moves two indices through a sequence in a coordinated way. With sorted data or another useful invariant, one pass can replace a nested scan, often reducing O(n²) work to O(n) without extra storage.

What this is

The technique works when the data has structure — usually sorted order — that lets you rule out large groups of candidates with a single comparison.

The Classic: Pair Sum

Find two numbers in a sorted array that sum to a target. Brute force checks all pairs: O(n²). Two pointers starts one at each end:

  • Sum too small? Move the left pointer right — only larger values lie that way.
  • Sum too large? Move the right pointer left — only smaller values lie that way.
  • Exactly right? Done.

Each move eliminates an entire set of pairs from consideration without ever testing them. The pointers only ever move toward each other, so the whole search is one pass.

Why Sorting Is Important

The technique depends on knowing what moving a pointer does. On a sorted array, moving left rightwards can only increase the sum — that guarantee is what makes it safe to discard everything you skipped.

On unsorted data no such guarantee exists, and the method is simply wrong. If your input is not sorted you must either sort first (O(n log n)) or use a hash set instead (O(n) time, O(n) space).

The three patterns

PatternPointersTypical use
Opposite endsStart and end, moving inwardsTwo-sum on sorted data, palindromes, container problems
Same directionBoth forward, at different speedsRemoving duplicates, partitioning in place
Fast and slowOne moves twice as fastCycle detection, finding the middle

Same direction is the in-place editing pattern. One pointer reads, the other writes:

def remove_duplicates(arr):          # arr is sorted; edit in place
    if not arr:
        return 0
    write = 1
    for read in range(1, len(arr)):
        if arr[read] != arr[write - 1]:
            arr[write] = arr[read]
            write += 1
    return write                     # new length

The invariant: everything before write is the deduplicated result so far. That framing — naming what is true about the region behind each pointer — is how these are reasoned about and debugged.

Fast and slow is the cycle-detection pattern, and the reason it works is worth understanding rather than memorising.

Container With Most Water

A subtler case worth understanding. Given heights, pick two lines forming the largest water container. Area is min(height) × width.

Start wide and always move the shorter line inward. Why is that safe? Moving the taller one can only reduce the width while the area stays capped by the same shorter line — so it can never improve. Moving the shorter one at least gives a chance of a taller limit. That single argument is what makes the greedy pointer movement correct.

Two indices instead of two loops

The two-pointer technique replaces a nested loop with two indices moving through the data, turning O(n²) into O(n).

It applies when the data has structure — usually sortedness — that tells you which pointer to move.

The canonical problem: find two numbers in a sorted array that sum to a target.

Brute force checks every pair: O(n²). With two pointers, one at each end:

Array [2, 7, 11, 15], target 18:

leftrightSumAction
21517Too small — move left right
71522Too large — move right left
71118Found

Three steps instead of six comparisons, and the saving grows with n. The logic is what makes it correct: if the sum is too small, the only way to increase it is a larger left value; if too large, a smaller right value. No possibility is skipped.

def two_sum_sorted(arr, target):
    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

Guided tour

  1. Run pair sum and step through. Watch each move discard a whole block of pairs the brute force would have tested.
  2. Compare the two counters. Two pointers does roughly n comparisons where brute force does n²/2.

Sources and next steps

Run it in Python

Three uses of the same idea — pair sum, in-place duplicate removal, and palindrome checking — each with its pointer positions printed, and each replacing a nested loop.

two_pointers.pyPython 3
Output

How the code works

  1. while lo < hi:Strictly less than, so the two pointers never land on the same element — which would pair a value with itself. Every two-pointer loop lives or dies on this condition.
  2. if total < target: lo += 1The move is justified, not guessed. The list is sorted, so with a[hi] as the largest available partner, a[lo] cannot be part of any solution — discarding it is safe.
  3. lo += 1 / hi -= 1Each step eliminates a whole row or column of the pair table, so the n² candidates are covered in n steps. That is the trick, and it only works because the input is sorted.
  4. write = 1; for read in range(1, len(a)):The other variant: both pointers move forwards, at different rates. write marks the end of the kept prefix and read scans ahead — nothing is allocated.
  5. while lo < hi and not text[lo].isalnum():The inner skips also need the lo < hi guard, or a string of pure punctuation runs a pointer off the end. Nested pointer loops are where the index errors hide.

Change one thing

  • Shuffle data before calling pair_sum. It returns None for a pair that exists — sortedness is a precondition, not a nicety.
  • Print values in full after dedupe. The tail is stale data, which is why the function returns a length rather than a list.
  • Extend pair_sum to three numbers: fix one, two-pointer the rest. O(n²) instead of O(n³), and the standard answer to 3Sum.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Predict, then reveal

About to run: set target sum to its maximum (30). Before it does — what happens to the readout?

Committing to an answer first is the point — the reveal runs the experiment on the visualisation above and reads the real value back, so nothing here is scripted.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Pair-sum with two pointers requires the list to be:

  2. When the sum is too small, why is it safe to move lo right rather than hi left?

  3. In the in-place dedupe, why does the function return a length instead of a list?

Cheat sheet

Two Pointers

Move two indices through a sorted array in one pass and replace an O(n-squared) nested loop with O(n) and no extra space.

ALGORITHMS · vizlearn.in/dsa/two_pointers.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.