Two Pointers

Naseebullah Ahmadi  Senior Software Engineer, London

Two indices walking through one ordered structure, discarding the side that cannot improve the answer at every step and replacing a nested loop with a single pass.

7 min read
#algorithms
In one line

Walk two indices through ordered data, and at every step, discard the side that cannot improve the answer.

time O(n)space O(1)

A brute-force scan compares every pair, which costs O(n^2). Two pointers works because order carries information. If the structure is sorted (or just monotonic), one comparison tells you which side can't improve, and you rule it out for good.

That single decision (move left or move right) is the whole pattern. Each element is visited once, and nothing is ever revisited.

When to use it

Reach for it

  • The input is sorted, or you can sort it cheaply.
  • You are looking for a pair, triplet, or boundary that satisfies a condition.
  • You need to partition, dedupe, or compact in place with O(1) extra space.
  • You are comparing a sequence against itself from both ends (palindromes, reversals).

Skip it

  • Order is meaningless and cannot be created (use hashing instead).
  • You need every qualifying pair, not the existence of one: the discarded side may hide answers.
  • The condition is not monotonic, so shrinking one side does not reliably move you toward the target.
Signals in the problem statement
  • "sorted array" in the first sentence
  • "find two numbers that…" / "closest to target"
  • "in place" or "O(1) extra space"
  • "remove duplicates" or "move all X to the end"
  • "is it a palindrome"

The pattern

  1. 1

    Place pointers at the two ends (opposite direction) or both at the start (same direction).

  2. 2Evaluate the condition at the current pair.
  3. 3

    Move the pointer that cannot possibly improve the result.

  4. 4Stop when the pointers meet or cross.
@itsnas pair-sum.ts
codepair-sum.ts
// Sorted array: does a pair sum to target?
function pairSum(
  nums: number[],
  target: number,
): [number, number] | null {
  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++ // need a bigger value
    else right-- // need a smaller value
  }
 
  return null
}
main
Nas (@itsnas)

Complexity

Time

Each element is visited at most once as the pointers close in.

Space

Just the two indices, no auxiliary structures.

Add O(nlogn)O(n \log n) up front if you have to sort the input yourself: that then dominates the total cost.

Common pitfalls

  1. 1

    Forgetting the input must be ordered

    The discard step is only valid under sorted or monotonic input. On unsorted data the pointers silently skip valid answers.

  2. 2

    Off-by-one on the loop guard

    Use left < right when a pair must be two distinct elements, and left <= right only when a single middle element is a valid answer.

  3. 3

    Not skipping duplicates

    In triplet and unique-pair problems, advance past equal neighbours after recording a hit, or the same answer repeats.

  4. 4

    Moving both pointers at once

    Unless the condition proves both sides are exhausted, moving both can step over the answer.

Variants worth knowing

  1. 1

    Opposite ends: pair sums, palindromes, container with most water.

  2. 2

    Fast and slow: cycle detection, middle of a linked list.

  3. 3Read and write: in-place dedupe and compaction.
  4. 4

    Sliding window: same-direction pointers with a running aggregate.

Practice, easiest first

End of entry · Keep exploring

What's next in the notebook?

Keep reading — more from where that came from.

Featured next
12 min read
0%

Migrating Schema-Per-Tenant Databases at Scale

Choosing physical tenant isolation over a shared, RLS-scoped schema buys two new problems: knowing where a tenant's data actually lives, and running one migration correctly hundreds of times instead of once. Neither has an app-code fix, both need their own infrastructure.

21 min read
#engineering

Designing Multi-Tenant APIs That Scale

A missing tenant filter is a data leak, not a crash. Row-level security fixes that structurally, but rate limits, connection pools, and error codes built for one instance break the same quiet way once the API runs as several.

0%
17 min read
#engineering

Why Payment Retries Need Idempotency

A plain payment endpoint looks correct until you trace what a double-click, a timed-out request, or a redelivered webhook actually does to it. Each one turns one payment into two. Idempotency keys are the fix, at two layers most write-ups skip.

0%
8 min read
#engineering, #frontend

Building a Typed Fetch Factory

How a single createFetcher factory infers request/response types from an OpenAPI schema and layers in caching, retries, and cancellation, and why each piece is built the way it is.

0%
8 min read
#algorithms

Valid Triangle Number

Sort the side lengths, fix the largest one at a time, and let the gap between two pointers count every valid pair against it in one step instead of testing them one by one.

0%
12 min read
#algorithms

3Sum

Sort the array, fix one value as a moving target, and reuse the exact two-pointer proof from Two Sum II to sweep the rest, with a couple of extra duplicate-skipping rules layered on top.

0%
8 min read
#algorithms

Two Sum II

A sorted array, two pointers closing in from both ends, and a proof that whichever side is off-target can be ruled out entirely rather than retried against a smaller search space.

0%
7 min read
#algorithms

Container With Most Water

A row of walls, two pointers starting at the widest container, and a proof that moving the taller wall can never beat what you already have, so only the shorter side is ever worth moving.

0%