Two Pointers

Published: May 7, 2026

Last updated: May 7, 2026

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:

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.

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.

Brute force

Target = 10

Every pair is checked until a match is found.

O(n^2) time | O(1) space

Sorted input

index 0

1

compare

index 1

2

compare

index 2

3

index 3

4

index 4

6

index 5

8

index 6

11

Step 1 of 11

Start with the first number

Set i = 0 and start the inner loop at j = 1.

State

left: -

right: -

current: -

compare: [0, 1]

sum/value: 3

answer: not yet

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.

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).

Two pointers on sorted input

Target = 10

Use the sorted order to decide which pointer to move.

O(n) time | O(1) space

Sorted input

index 0

1

leftcompare

index 1

2

index 2

3

index 3

4

index 4

6

index 5

8

index 6

11

rightcompare

Step 1 of 4

Start at both ends

1 + 11 = 12. Too large, so move the right pointer left.

State

left: 0

right: 6

current: -

compare: [0, 6]

sum/value: 12

answer: not yet

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.

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.

Sort, then scan

Target = 10

Useful when the original input is not sorted but you still want the two-pointer idea.

O(n log n) time | O(n) space

Sorted value/index pairs

index 0

1 (orig 1)

index 1

2 (orig 5)

index 2

3 (orig 6)

index 3

4 (orig 4)

index 4

6 (orig 2)

index 5

8 (orig 0)

index 6

11 (orig 3)

Step 1 of 6

Decorate the input

Pair each number with its original index so you can sort without losing the answer positions.

State

left: -

right: -

current: -

compare: -

sum/value: -

answer: not yet

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.

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.

Hash map lookup

Target = 10

Store seen values and look for each number's complement.

O(n) time | O(n) space

Unsorted input

index 0

8

current

index 1

1

index 2

6

index 3

11

index 4

4

index 5

2

index 6

3

Step 1 of 6

Visit index 0

Need 2 to complete 8, so store 8 -> 0 and continue.

State

left: -

right: -

current: 0

compare: -

sum/value: 8

answer: not yet

Seen map

8 -> 0

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.

Inward traversal

Start from both ends and shrink toward the middle.

Example: checking if "racecar" is a palindrome

index 0

r

left

index 1

a

index 2

c

index 3

e

index 4

c

index 5

a

index 6

r

right

Step 1 of 7

Initialize both pointers

Place left at the first character and right at the last character.

State

fixed: -

left: 0

right: 6

stage: single pass

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.

Unidirectional traversal

Both pointers move left-to-right, but they do different jobs.

Example: removing duplicates from [1, 1, 2, 3, 3]

index 0

1

left

index 1

1

right

index 2

2

index 3

3

index 4

3

Step 1 of 7

Reader and writer start near the front

The slow pointer marks where the next unique value should go. The fast pointer scans ahead.

State

fixed: -

left: 0

right: 1

stage: single pass

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.

Staged traversal

One pointer establishes context first, then other pointers take over inside that context.

Example: 3Sum on [-4, -1, -1, 0, 1, 2]

index 0

-4

index 1

-1

fixed

index 2

-1

index 3

0

index 4

1

index 5

2

Step 1 of 6

Stage 1: fix one value

Choose a pivot index first. In 3Sum, this pointer sets the first number in the triplet.

State

fixed: 1

left: -

right: -

stage: fixed pivot

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.

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; }

Is Subsequence

Two pointers across two strings

The pointer in `s` only moves on a match. The pointer in `t` keeps scanning forward.

O(|s| + |t|) time | O(1) space

Current case

s: abc

t: ahbgdc

s (the subsequence we want to match)

index 0

a

pointer

index 1

b

index 2

c

t (the string we scan through)

index 0

a

pointer

index 1

h

index 2

b

index 3

g

index 4

d

index 5

c

Step 1 of 8

Start at the first characters

Compare s[0] = a with t[0] = a.

State

i in s: 0

j in t: 0

matched chars: 0

result: in progress

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.

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.

Two Sum II

Sorted array with 1-indexed output

Same two-pointer idea as pair sum, but return positions starting from 1 instead of 0.

O(n) time | O(1) space

numbers | target = 9

index 1

2

left

index 2

7

index 3

11

index 4

15

right

Step 1 of 5

Start from both ends

Initialize left at index 0 and right at index 3.

State

left: 0 (returns 1)

right: 3 (returns 4)

sum: 17

answer: not yet

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.

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.

3Sum

Fixed index + inner two-pointer scan

This is a staged traversal: one outer pointer fixes a value, then the remaining suffix is searched with left and right pointers.

O(n^2) time | O(1) extra space (excluding output)

Sorted input

index 0

-4

fixed

index 1

-1

left

index 2

-1

index 3

0

index 4

1

index 5

2

right

Step 1 of 14

Sort first

3Sum becomes much easier once the array is sorted because the inner search can use the same left/right reasoning as pair sum.

Results so far

i: 0

left: 1

right: 5

sum: -3

triplets: none yet

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.

Personal image

Dennis O'Keeffe

Byron Bay, Australia

Dennis O'Keeffe

2020-present Dennis O'Keeffe.

All Rights Reserved.