# Two Pointers

> Learn how to spot two-pointer problems and walk through pair sum variants with interactive visuals

- Canonical: https://dennisokeeffe.com/blog/2026-05-07-two-pointers
- Published: 2026-05-07
- Updated: 2026-05-07
- Tags: interviews, algorithms

Two pointers is one of the highest ROI patterns for interview prep.

The main reason is not that the implementation is complicated. It usually is not. The value is in learning how to recognize when the problem is giving you permission to do less work.

If an array is sorted, or if a string can be scanned from both ends, or if you are really maintaining a shrinking window, there is a good chance a pointer-based approach is available.

This post starts with a walkthrough of one of the more common patterns you begin with online and then adds some other examples and visuals for other solutions.

## Pair Sum - Sorted

Let us start with a classic prompt:

> Given a sorted array of integers `nums` and a target `target`, return the indices of the two numbers that add up to the target.

Example input:

```ts
const nums = [1, 2, 3, 4, 6, 8, 11];
const target = 10;
```

The expected answer is `[1, 5]` because `nums[1] + nums[5] = 2 + 8 = 10`.

## Brute force intuition

The most direct thought is normally: try every pair.

```ts
export function pairSumSortedBruteForce(nums, target) {
  for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] + nums[j] === target) {
        return [i, j];
      }
    }
  }
  return [];
}
```

This is a perfectly fine place to begin in an interview because:

1. It is correct.
2. It gives you a baseline.
3. It makes the inefficiency obvious.

The problem is the nested loops. In the worst case, you check almost every pair, which gives `O(n^2)` time.

_[Interactive PairSumWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
What the brute-force version misses is that the input is already sorted.

That one detail changes everything.

## The two pointers idea

If the array is sorted, then the leftmost value is the smallest and the rightmost value is the largest.

That means each comparison gives us directional information:

1. If the sum is too small, move `left` right to increase the sum.
2. If the sum is too large, move `right` left to decrease the sum.
3. If the sum matches, we are done.

Instead of restarting work for every element, we keep squeezing the search space inward.

```ts
export function pairSumSorted(nums, target) {
  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 += 1;
    } else {
      right -= 1;
    }
  }

  return [];
}
```

This drops the runtime to `O(n)` and keeps space at `O(1)`.

_[Interactive PairSumWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_

  A useful interview sentence here is: "Because the array is sorted, each
  comparison lets us eliminate part of the search space."


## Why the sorted property matters

Without sorting, moving a pointer left or right would not reliably tell us whether the sum should go up or down.

With sorting:

1. Moving `left` right always gives a value that is the same or larger.
2. Moving `right` left always gives a value that is the same or smaller.

That monotonic behavior is what makes the pointer movement safe.

## Faster alternatives when the input is not already sorted

This post is centered on the sorted version of the problem, but in practice you will often be asked the unsorted variant too.

There are two common follow-up paths.

## Sort first, then scan

If you want to keep the two-pointer strategy, you can sort first.

The catch is that if the problem asks for the original indices, you need to keep them around while sorting.

```ts
export function pairSumWithSort(nums, target) {
  const pairs = nums.map((value, index) => ({ value, index }));
  pairs.sort((a, b) => a.value - b.value);

  let left = 0;
  let right = pairs.length - 1;

  while (left < right) {
    const sum = pairs[left].value + pairs[right].value;

    if (sum === target) {
      return [pairs[left].index, pairs[right].index];
    }

    if (sum < target) {
      left += 1;
    } else {
      right -= 1;
    }
  }

  return [];
}
```

Complexity:

1. `O(n log n)` time because sorting dominates.
2. `O(n)` space because we store value/index pairs.

_[Interactive PairSumWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
I like this version when the conversation is specifically about pointer patterns, because it shows you understand how to preserve the property you need.

## Hash map lookup

If the array is unsorted and you want the fastest lookup-based solution, a hash map is usually the better fit.

```ts
export function pairSumHashMap(nums, target) {
  const seen = new Map();

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];

    if (seen.has(complement)) {
      return [seen.get(complement), i];
    }

    seen.set(nums[i], i);
  }

  return [];
}
```

Complexity:

1. `O(n)` time.
2. `O(n)` space.

_[Interactive PairSumWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
The trade-off is clean:

1. Two pointers needs sorted order but uses constant extra space.
2. Hash map works on unsorted input but spends extra memory.

## Identifying the pattern

When I read a problem like this, I mentally check for these clues.

### 1. Is the input sorted?

This is the loudest hint.

If the data is sorted and the task involves pairs, ranges, or comparing extremes, two pointers should immediately be on the shortlist.

### 2. Am I asked about a pair, triplet, or contiguous range?

Problems involving:

1. two numbers,
2. three numbers,
3. a window over a string or array,

often become pointer problems once ordering is useful.

### 3. Does each comparison eliminate possibilities?

This is the real question.

If one comparison tells you "everything to the left is now too small" or "everything to the right is now too large", the problem is probably pointer-friendly.

### 4. Am I repeatedly rescanning work?

If your brute-force version uses nested loops, ask whether a pointer can carry forward information from the last comparison instead of starting over.

## Tips and tricks

### Say the invariant out loud

For this problem, the invariant is that the answer, if it exists, must stay inside the current `[left, right]` range.

When the sum is too small, moving `right` would only make things smaller or equal, so the only useful move is `left += 1`.


  Here, the "invariant" means a condition or fact that stays true throughout the
  algorithm while the loop is running.


### Ask whether the array is already sorted

If the prompt does not say so, ask.

That one clarification can change the best solution from hash map to two pointers.

### Watch the output requirements

If the question wants original indices and you decide to sort, preserve the indices before sorting.

### Distinguish pattern from implementation

The pattern is not just "have two variables called left and right".

The pattern is using structure in the input so pointer movement is meaningful.

## Two-pointer traversals

There are three two-pointer strategies to know about:

1. Inward traversal.
2. Unidirectional traversal.
3. Staged traversal.

### Inward traversal

The two pointers start at opposing ends and work towards each other.

_[Interactive TraversalWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
Common examples:

1. `Valid Palindrome`
2. `Two Sum II - Input Array Is Sorted`
3. `Container With Most Water`

### Unidirectional traversal

Both pointers start at the same end of the data structure.

_[Interactive TraversalWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
Common examples:

1. `Is Subsequence`
2. `Remove Duplicates From Sorted Array`
3. `Move Zeroes`

### Staged traversal

One pointer establishes context first, and then another pointer pair or follow-up pointer traversal takes over within that smaller search space.

_[Interactive TraversalWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
Common examples:

1. `3Sum`
2. `4Sum`
3. `Trapping Rain Water` when you reason in phases around boundary pointers

## Similar problems worth practicing

These names are often used as standard interview-problem titles, but the useful part is the pattern hiding underneath each one.

### Valid Palindrome: compare characters from the outside in

You place one pointer at the start of the string and one at the end.

If the characters match, move both inward. If they do not, the string is not a palindrome. This is one of the clearest examples of two pointers on a string.

### Is Subsequence: scan one string while trying to consume the other

This is another useful string variant, but the pointers do different jobs.

1. One pointer walks through `s`, the smaller string we want to match.
2. The other pointer walks through `t`, the larger string we are scanning.
3. When the characters match, advance both pointers.
4. When they do not match, only advance the pointer in `t`.

If the pointer in `s` reaches the end, then every character in `s` appeared in order inside `t`.

```ts
function isSubsequence(s: string, t: string): boolean {
  let i = 0,
    j = 0;

  while (i < s.length && j < t.length) {
    if (s[i] === t[j]) {
      i++;
    }

    j++;
  }

  return i === s.length;
}
```

_[Interactive IsSubsequenceWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
### Two Sum II: find a pair in a sorted array

This is basically the same shape as the pair sum problem we just walked through.

The array is sorted, so each sum tells you whether to move the left pointer right or the right pointer left.

The main twist is that the answer must be returned as **1-indexed** positions.

One small correction to the implementation you pasted: `right` should start at `numbers.length - 1`, not `numbers.length`, otherwise the first lookup is out of bounds.

```ts
function twoSum(numbers: number[], target: number): number[] {
  let left = 0;
  let right = numbers.length - 1;

  while (left < right) {
    const sum = numbers[left] + numbers[right];

    if (sum === target) {
      return [left + 1, right + 1];
    }

    if (sum < target) {
      left++;
    } else {
      right--;
    }
  }

  return [];
}
```

Quick guidance:

1. `left` starts at the smallest value.
2. `right` starts at the largest value.
3. If the sum is too small, move `left` right.
4. If the sum is too large, move `right` left.
5. Return `left + 1` and `right + 1` because the problem is 1-indexed.

_[Interactive TwoSumIIWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
### 3Sum: reduce a triplet problem into repeated pair searches

You sort the array, fix one number, and then use two pointers on the remaining suffix to look for the other two values.

This is a good example of how one pointer pattern can become a building block inside a larger solution.

```ts
function threeSum(nums: number[]): number[][] {
  nums.sort((a, b) => a - b);
  const results: number[][] = [];
  const n = nums.length;

  for (let i = 0; i < n - 2; i++) {
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = n - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        results.push([nums[i], nums[left], nums[right]]);

        while (left < right && nums[left] === nums[left + 1]) left++;
        while (left < right && nums[right] === nums[right - 1]) right--;

        left++;
        right--;
      } else if (sum < 0) {
        left++;
      } else {
        right--;
      }
    }
  }

  return results;
}
```

Notes on the solution:

1. Sorting is what makes the inner `left` and `right` movement meaningful.
2. The outer loop fixes one value, then the inner loop solves a pair-style search for the remaining two values.
3. You must skip duplicate fixed values, or you will produce duplicate triplets.
4. After finding a valid triplet, you also skip duplicate `left` and `right` values before continuing.
5. The runtime is `O(n^2)` because for each fixed index, the inner pointers scan the remaining suffix once.

_[Interactive ThreeSumWalkthrough: view it at https://dennisokeeffe.com/blog/2026-05-07-two-pointers]_
### Container With Most Water: move the side that limits the answer

You start with pointers at both ends and compute the area formed by the two heights.

The key move is to advance the shorter wall, because the shorter wall is currently limiting the area.

### Remove Duplicates From Sorted Array: use a slow writer and a fast reader

This is still a two-pointers problem, but the pointers do different jobs.

One pointer scans forward through the array while the other marks where the next unique value should be written.

### Squares of a Sorted Array: compare the extremes and fill from the back

When negatives are involved, the largest square may come from either end of the sorted array.

So you compare both ends, place the larger square into the result from right to left, and move the pointer that produced it.

## A quick decision guide

If I had to compress the choice into a few lines:

1. Already sorted and need a pair: start with two pointers.
2. Unsorted and need the fastest lookup: use a hash map.
3. Unsorted but want to leverage ordering: sort and then run two pointers.

## What comes next

I will keep extending this post with more two-pointers examples.

Next up will likely be problems such as valid palindrome, where the same idea shows up in strings instead of arrays.

The main thing I want to reinforce is this: do not memorize the pointer movement rules in isolation. Train yourself to ask what property of the input makes pointer movement safe.

That is the part that transfers across problems.
