Two PointersArraysStringsSorted DataO(n) Optimization

Two Pointers — The Complete Guide

Master the Two Pointers pattern from first principles. Learn to recognize it instantly, evolve brute-force into optimal solutions, and confidently solve interview questions.

35 min read10 sections
01

Intuition from Scratch

Why does this pattern exist?

Imagine you have a sorted list of numbers and someone asks: "Find two numbers that add up to 15." The brute-force way is to check every possible pair — pick the first number, then scan the rest. That's O(n²). Painfully slow for large inputs.

But wait — the list is sorted. That's a massive clue. If you place one finger at the start (smallest number) and another at the end (largest number), you can make intelligent decisions:

  • Sum too small? Move the left finger right (need a bigger number)
  • Sum too big? Move the right finger left (need a smaller number)
  • Sum just right? Found it.

Each step eliminates an entire row or column of possibilities. You never go backwards. One pass, O(n). That's the Two Pointers pattern — using two indices that move strategically to avoid redundant work.

Analogies to Build Intuition

📖

The Dictionary Search

Looking for a word in a physical dictionary. You open to the middle — too far? Go left. Not far enough? Go right. Two Pointers is like having two bookmarks, one near the front and one near the back, and moving them inward based on what you find.

🤝

Two People Walking Toward Each Other

Imagine two people at opposite ends of a hallway. They walk toward each other, each adjusting their speed based on a condition. They'll eventually meet in the middle. That's the opposite-direction variant — you process the entire array in one pass.

🏃

The Slow and Fast Runner

Two runners on a track, one fast and one slow. If the track has a loop, the fast runner will eventually lap the slow one — that's how you detect cycles. If there's no loop, the fast runner reaches the end first — that's how you find the middle.

What kind of problems does it solve?

Two Pointers is your go-to when:

  • You're working with a sorted array or string
  • You need to find pairs or triplets with a specific property
  • You need to partition or rearrange elements in-place
  • You need to compare or merge two sequences
  • You want to reduce O(n²) brute force to O(n)

The core insight

Two Pointers works because each pointer movement eliminates possibilities that you'd otherwise check in a nested loop. The sorted order (or some other structural property) guarantees that skipped possibilities can't contain the answer.

02

Pattern Recognition

Recognizing Two Pointers in the wild is the most important skill. Here are the signals to look for and the traps to avoid.

Keywords & Signals in the Problem Statement

Use Two Pointers when you see...

  • "Sorted array" + find pair/triplet with sum X
  • "In-place" removal or rearrangement
  • "Palindrome" checking or manipulation
  • "Merge two sorted arrays/lists"
  • "Container with most water" / area problems
  • "Remove duplicates from sorted array"
  • "Partition array" around a pivot
  • "Subsequence" check (is A a subsequence of B?)
  • "Closest pair" to a target value
  • O(1) extra space constraint on array problems

Don't use Two Pointers when...

  • Array is unsorted AND you can't sort it (use Hashing instead)
  • You need contiguous subarray sum/length (use Sliding Window)
  • Problem requires all combinations/subsets (use Backtracking)
  • You need to find elements by value in unsorted data (use Hash Map)
  • The problem involves a tree or graph structure
  • You need the count of subarrays (usually Prefix Sum or Sliding Window)

Two Pointers vs. Similar Patterns

PatternWhen to UseKey Difference
Two PointersSorted data, pairs, in-place operationsPointers move based on comparison logic, not window size
Sliding WindowContiguous subarray/substring with constraintWindow expands/shrinks to maintain a condition — both pointers move right
Binary SearchFind a specific value or boundary in sorted dataHalves the search space each step — only one "pointer" (mid) moves
HashingUnsorted data, need O(1) lookupTrades space for time — no pointer movement, just map lookups

The sorting question

If the input isn't sorted but the problem screams "Two Pointers," ask yourself: "Can I sort it first?" If sorting doesn't break the problem (e.g., you don't need original indices), sort it and then apply Two Pointers. This is exactly how 3Sum works.

03

Brute Force → Optimized Thinking

Let's walk through the thinking evolution using a concrete example: Two Sum II — given a sorted array, find two numbers that add up to a target.

1

Brute Force — Check Every Pair

O(n²) time · O(1) space

The most naive approach: for each element, scan every element after it and check if they sum to the target. This works but is slow because we're doing redundant comparisons.

Brute Forcetypescript
function twoSum(nums: number[], target: number): number[] {
  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 + 1, j + 1];
    }
  }
  return [];
}
// Problem: We're ignoring the fact that the array is SORTED.
2

Better — Binary Search for Complement

O(n log n) time · O(1) space

Since the array is sorted, for each element nums[i], we can binary search for (target - nums[i]) in the remaining array. This uses the sorted property but still has an outer loop.

Binary Search Approachtypescript
function twoSum(nums: number[], target: number): number[] {
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    // Binary search for complement in nums[i+1..end]
    let lo = i + 1, hi = nums.length - 1;
    while (lo <= hi) {
      const mid = lo + Math.floor((hi - lo) / 2);
      if (nums[mid] === complement) return [i + 1, mid + 1];
      if (nums[mid] < complement) lo = mid + 1;
      else hi = mid - 1;
    }
  }
  return [];
}
// Better! But we can do even better by using BOTH ends.
3

Optimal — Two Pointers from Both Ends

O(n) time · O(1) space

The key insight: place one pointer at the start (smallest) and one at the end (largest). The sum of these two tells us exactly which pointer to move. If the sum is too small, we need a bigger number → move left pointer right. If too big → move right pointer left. Each step eliminates one candidate, so we process the entire array in a single pass.

Two Pointers — Optimaltypescript
function twoSum(nums: number[], target: number): number[] {
  let left = 0, right = nums.length - 1;
  while (left < right) {
    const sum = nums[left] + nums[right];
    if (sum === target) return [left + 1, right + 1];
    if (sum < target) left++;   // need bigger sum → move left forward
    else right--;               // need smaller sum → move right backward
  }
  return [];
}
// Perfect: O(n) time, O(1) space. Uses the sorted property fully.

Why Does Each Optimization Work?

1

Brute → Binary Search

We realized the inner loop is just 'searching for a value in a sorted array' — that's exactly what binary search does. We replaced O(n) inner scan with O(log n) binary search.

2

Binary Search → Two Pointers

We realized that after checking a pair, the sorted order tells us WHICH direction to move. We don't need to restart the search — we can maintain two pointers and converge. This eliminates the outer loop entirely.

3

The General Principle

Each optimization exploits more of the problem's structure. Brute force ignores everything. Binary search uses 'sorted'. Two Pointers uses 'sorted' + 'the answer about the current pair tells us which pointer to move'. The more structure you exploit, the faster the solution.

The thinking process matters more than the code

In interviews, walking through this brute → better → optimal progression shows the interviewer how you think. Even if you know the optimal solution immediately, briefly mention the brute force and explain why you're optimizing. It demonstrates depth of understanding.

04

Core Templates

Two Pointers has three main templates. Memorize these structures — most problems are variations of one of them.

Template 1: Opposite Direction (Converging)

Pointers start at both ends and move toward each other. Used for pair-finding in sorted arrays, palindrome checks, and container problems.

Opposite Direction Templatetypescript
function oppositeDirection(arr: number[]): Result {
  let left = 0;
  let right = arr.length - 1;

  while (left < right) {
    // Compute something with arr[left] and arr[right]
    const current = compute(arr[left], arr[right]);

    if (current === target) {
      return result;           // found answer
    } else if (current < target) {
      left++;                  // need bigger → move left forward
    } else {
      right--;                 // need smaller → move right backward
    }
  }

  return notFound;
}
// When to use: sorted array + pair search, palindrome, container/area

Template 2: Same Direction (Fast & Slow / Read & Write)

Both pointers start at the beginning. One moves faster or conditionally. Used for in-place removal, deduplication, and partitioning.

Same Direction Templatetypescript
function sameDirection(arr: number[]): number {
  let slow = 0; // write pointer — marks the "good" portion

  for (let fast = 0; fast < arr.length; fast++) {
    // fast = read pointer, scans every element
    if (shouldKeep(arr[fast])) {
      arr[slow] = arr[fast];   // write to the "good" portion
      slow++;
    }
  }

  return slow; // length of the "good" portion
}
// When to use: remove duplicates, remove element, move zeroes

Template 3: Two Sequences

One pointer per sequence. Used for merging sorted arrays, checking subsequences, and comparing strings.

Two Sequences Templatetypescript
function twoSequences(arr1: number[], arr2: number[]): Result {
  let i = 0; // pointer for arr1
  let j = 0; // pointer for arr2

  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) {
      // process arr1[i]
      i++;
    } else if (arr1[i] > arr2[j]) {
      // process arr2[j]
      j++;
    } else {
      // arr1[i] === arr2[j] — match found
      // process both
      i++;
      j++;
    }
  }

  // Handle remaining elements in either array
  return result;
}
// When to use: merge sorted arrays, intersection, is-subsequence

Which template to pick?

Ask yourself: (1) Am I searching for a pair in one sorted array? → Opposite Direction. (2) Am I filtering/rearranging elements in-place? → Same Direction. (3) Am I comparing or merging two separate sequences? → Two Sequences.

05

Variations & Sub-Patterns

Two Pointers isn't a single trick — it's a family of techniques. Here are the most common variations and how the approach changes for each.

VariationPointer MovementClassic Example
Pair Sum (sorted)left→ and ←right converge based on sum comparisonTwo Sum II, 3Sum
Palindrome Checkleft→ and ←right converge, comparing charactersValid Palindrome, Palindrome Linked List
Container / Arealeft→ and ←right converge, move the shorter sideContainer With Most Water, Trapping Rain Water
Remove / Deduplicateslow (write) + fast (read) move same directionRemove Duplicates, Remove Element, Move Zeroes
Partition (Dutch Flag)Three pointers: low, mid, highSort Colors (0s, 1s, 2s)
Merge Two SortedOne pointer per array, advance the smallerMerge Sorted Array, Merge Two Sorted Lists
Subsequence CheckOne pointer per string, advance both on matchIs Subsequence
Three Sum / K-SumFix one element, two-pointer on the rest3Sum, 4Sum, 3Sum Closest

Deep Dive: The 3Sum Technique

3Sum is the most important Two Pointers extension. The idea: reduce a 3-element problem to a 2-element problem by fixing one element and running Two Pointers on the rest. This generalizes to K-Sum.

Problems mentioned above

3Sum — Sort + Fix + Two Pointerstypescript
function threeSum(nums: number[]): number[][] {
  nums.sort((a, b) => a - b);
  const result: number[][] = [];

  for (let i = 0; i < nums.length - 2; i++) {
    // Skip duplicates for the fixed element
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = nums.length - 1;
    const target = -nums[i]; // we need nums[left] + nums[right] = -nums[i]

    while (left < right) {
      const sum = nums[left] + nums[right];
      if (sum === target) {
        result.push([nums[i], nums[left], nums[right]]);
        // Skip duplicates
        while (left < right && nums[left] === nums[left + 1]) left++;
        while (left < right && nums[right] === nums[right - 1]) right--;
        left++;
        right--;
      } else if (sum < target) {
        left++;
      } else {
        right--;
      }
    }
  }

  return result;
}
// Time: O(n²) — can't do better for 3Sum
// Space: O(1) ignoring output

Deep Dive: The Dutch National Flag (3-Way Partition)

When you need to partition an array into three groups (e.g., Sort Colors: 0s, 1s, 2s), use three pointers: low, mid, and high. Elements before low are group 1, between low and mid are group 2, after high are group 3.

Sort Colors — Dutch National Flagtypescript
function sortColors(nums: number[]): void {
  let low = 0, mid = 0, high = nums.length - 1;

  while (mid <= high) {
    if (nums[mid] === 0) {
      [nums[low], nums[mid]] = [nums[mid], nums[low]];
      low++;
      mid++;
    } else if (nums[mid] === 1) {
      mid++;
    } else {
      [nums[mid], nums[high]] = [nums[high], nums[mid]];
      high--;
      // Don't advance mid — the swapped element needs checking
    }
  }
}
// Time: O(n) single pass | Space: O(1)
06

Visual Walkthrough

Let's trace through Container With Most Water step by step to see Two Pointers in action. Input: [1, 8, 6, 2, 5, 4, 8, 3, 7].

Container With Most Water — Step-by-Step Tracetext
Array: [1, 8, 6, 2, 5, 4, 8, 3, 7]
Index:  0  1  2  3  4  5  6  7  8

Step 1: L=0(1), R=8(7) → area = min(1,7) × 8 = 8move L (1 < 7)
Step 2: L=1(8), R=8(7) → area = min(8,7) × 7 = 49move R (7 < 8)  ★ best so far
Step 3: L=1(8), R=7(3) → area = min(8,3) × 6 = 18move R (3 < 8)
Step 4: L=1(8), R=6(8) → area = min(8,8) × 5 = 40move R (equal, either works)
Step 5: L=1(8), R=5(4) → area = min(8,4) × 4 = 16move R (4 < 8)
Step 6: L=1(8), R=4(5) → area = min(8,5) × 3 = 15move R (5 < 8)
Step 7: L=1(8), R=3(2) → area = min(8,2) × 2 = 4move R (2 < 8)
Step 8: L=1(8), R=2(6) → area = min(8,6) × 1 = 6move R (6 < 8)
         L=1, R=1L not < R, stop.

Answer: 49 (between index 1 and 8)

Key insight: We always move the SHORTER wall because:
- Area = min(left, right) × width
- Width always decreases by 1
- Moving the taller wall: min can only stay same or decreasearea decreases
- Moving the shorter wall: min might increasearea might increase
So moving the shorter wall is the only move that has a CHANCE of improving.

Trapping Rain Water — The Harder Variant

Trapping Rain Water — Two Pointers Tracetext
Array: [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
Index:  0  1  2  3  4  5  6  7  8  9  10 11

leftMax = 0, rightMax = 0, water = 0

Step 1:  L=0, R=11 | leftMax=0, rightMax=1
         height[L]=0 <= height[R]=1process left
         leftMax = max(0, 0) = 0 | water += 0 - 0 = 0 | L++

Step 2:  L=1, R=11 | leftMax=1, rightMax=1
         height[L]=1 <= height[R]=1process left
         leftMax = max(0, 1) = 1 | water += 1 - 1 = 0 | L++

Step 3:  L=2, R=11 | leftMax=1, rightMax=1
         height[L]=0 <= height[R]=1process left
         leftMax = 1 | water += 1 - 0 = 1 ★ | L++

...continues until L meets R...

Final answer: 6 units of water

The trick: always process the side with the smaller max.
That side's water level is determined — the other side is at least as tall.
07

Practice Problems

These 7 problems are carefully ordered from easy to hard. Each one teaches a different aspect of Two Pointers. Solve them in order.

1

LC 125. Valid Palindrome

Easy

Why this pattern:

Opposite direction — compare characters from both ends, skip non-alphanumeric. This is the simplest Two Pointers problem and builds the core muscle of converging pointers.

Key idea: Initialize left = 0, right = end. Skip non-alphanumeric characters. Compare lowercase versions. If mismatch → not a palindrome. If pointers cross → it is.

2

LC 167. Two Sum II — Input Array Is Sorted

Medium

Why this pattern:

Opposite direction — the sorted property means the sum of left + right tells you exactly which pointer to move. This is the textbook Two Pointers problem.

Key idea: left = 0, right = end. If sum < target, move left right. If sum > target, move right left. The sorted order guarantees you won't miss the answer.

3

LC 283. Move Zeroes

Easy

Why this pattern:

Same direction (read/write) — fast pointer reads every element, slow pointer writes non-zero elements. After the loop, fill the rest with zeroes.

Key idea: slow = write position for non-zero values. Fast scans everything. When fast finds a non-zero, swap it to the slow position. This maintains relative order of non-zero elements.

4

LC 15. 3Sum

Medium

Why this pattern:

Sort + fix one element + opposite-direction Two Pointers on the rest. The key challenge is handling duplicates — skip duplicate values for all three positions.

Key idea: Sort the array. For each nums[i], run Two Pointers on nums[i+1..end] looking for pairs that sum to -nums[i]. Skip duplicates at every level to avoid duplicate triplets.

5

LC 11. Container With Most Water

Medium

Why this pattern:

Opposite direction — the width decreases as pointers converge, so you must move the shorter wall (it's the bottleneck). Moving the taller wall can never increase the area.

Key idea: Area = min(height[left], height[right]) × (right - left). Always move the pointer pointing to the shorter wall. The shorter wall limits the area, so keeping it can't help.

6

LC 75. Sort Colors

Medium

Why this pattern:

Dutch National Flag — three pointers (low, mid, high) partition the array into three sections in a single pass. This is the 3-way partition variant.

Key idea: low tracks where the next 0 goes, high tracks where the next 2 goes, mid scans. Swap 0s to low, 2s to high, skip 1s. Don't advance mid after swapping with high (the swapped value is unchecked).

7

LC 42. Trapping Rain Water

Hard

Why this pattern:

Opposite direction — at each position, the water trapped depends on the minimum of the max height to its left and right. Two pointers track these maxes from both ends, converging inward.

Key idea: Maintain leftMax and rightMax. Process the side with the smaller max (it's the bottleneck). Water at position = currentMax - height[position]. Move that pointer inward. The other side's max can only be >= current, so the calculation is safe.

Practice strategy

For each problem: (1) Identify which template applies before coding. (2) Write the brute force first. (3) Optimize using Two Pointers. (4) After solving, write one sentence: "This is [template] because [signal]."

08

Common Mistakes

These are the traps that catch people on Two Pointers problems. Learn them here so you don't learn them in an interview.

🔄

Forgetting to skip duplicates in 3Sum

You get the right triplets but with duplicates like [-1,0,1] appearing multiple times. The interviewer asks you to fix it and you panic.

After finding a valid triplet, advance both pointers past duplicate values: while (left < right && nums[left] === nums[left+1]) left++. Do the same for the outer loop's fixed element.

📐

Using left <= right instead of left < right

When pointers can be equal, you might process the same element twice (e.g., using the same number twice in Two Sum when it shouldn't be allowed).

For pair problems, use left < right (strict). The only time you'd use <= is when you need to process the element where they meet (rare).

🔀

Applying Two Pointers to unsorted data

You see 'find a pair with sum X' and immediately use Two Pointers, but the array isn't sorted. The pointer movement logic breaks because there's no monotonic property.

Always check: is the data sorted? If not, either sort it first (if you don't need original indices) or use a Hash Map instead.

🏃

Not advancing mid after swapping with low in Dutch Flag

In Sort Colors, you swap nums[mid] with nums[low] but forget to advance mid. This causes an infinite loop or incorrect results.

When swapping with low, advance BOTH low and mid (the swapped value from low is already processed). When swapping with high, only decrement high (the swapped value from high is unprocessed).

🎯

Moving the wrong pointer

In Container With Most Water, you move the taller wall's pointer instead of the shorter one. Or in Two Sum, you move left when you should move right.

Always reason about what each pointer movement does to the result. In container problems: moving the shorter wall might increase area, moving the taller wall never can. Write this reasoning as a comment before coding.

Confusing Two Pointers with Sliding Window

You use converging Two Pointers on a problem that needs a sliding window (both pointers moving right). The approaches look similar but solve different problem types.

Two Pointers: pointers move toward each other OR one is fast/slow. Sliding Window: both pointers move in the same direction maintaining a 'window'. If the problem says 'contiguous subarray with constraint', it's Sliding Window.

09

Interview Strategy

Knowing the pattern is half the battle. Here's how to present it in an interview setting to maximize your score.

The 5-Step Interview Flow

1

Clarify & Confirm

'The array is sorted, correct? And I need to find a pair that sums to the target? Can there be duplicates? Should I return indices or values?' — Clarifying shows you're thorough and buys you thinking time.

2

State the Brute Force

'The brute force would be to check every pair — that's O(n²). But since the array is sorted, I can do better.' — This shows you understand the baseline and sets up the optimization.

3

Explain the Optimization

'I'll use Two Pointers — one at the start, one at the end. Since the array is sorted, if the sum is too small I move the left pointer right, if too big I move the right pointer left. This gives me O(n).' — Name the pattern explicitly.

4

Code It Clean

Write the solution. Use descriptive variable names (left/right, not i/j). Add a brief comment for the key decision point. Keep it concise — no unnecessary variables or logic.

5

Test & Analyze

Trace through an example: 'For [2,7,11,15] with target 9: left=0(2), right=3(15), sum=17 > 9 → right--. left=0(2), right=2(11), sum=13 > 9 → right--. left=0(2), right=1(7), sum=9 = target ✓'. State complexity: O(n) time, O(1) space.

Common Follow-Up Questions

Follow-UpHow to Handle
"What if the array isn't sorted?"Use a Hash Map for O(n) time, O(n) space. Or sort first for O(n log n) time, O(1) space if indices don't matter.
"Can you find all pairs, not just one?"Same Two Pointers approach, but don't return early — collect all valid pairs. Handle duplicates by skipping equal values.
"Extend to 3Sum / 4Sum?"Fix one (or two) elements with an outer loop, then run Two Pointers on the rest. Time becomes O(n^(k-1)) for k-Sum.
"What about duplicates?"After finding a match, skip duplicate values: while (left < right && nums[left] === nums[left+1]) left++. Same for right and the outer loop.
"Can you do it with less space?"Two Pointers already uses O(1) space — that's the best possible. Mention this as an advantage over the Hash Map approach.
"What if there are multiple valid answers?"Clarify: do you want all of them, the closest one, or any one? This changes whether you return early or continue scanning.

The meta-skill

Interviewers aren't just testing if you know Two Pointers. They're testing if you can (1) recognize when to use it, (2) explain WHY it works, and (3) adapt when the problem changes. Practice explaining your reasoning out loud — it's a different skill from solving silently.

10

Summary & Cheat Sheet

Pin this section. Review it before mock interviews and contests.

Quick Revision Cheat Sheet

Core idea: Two indices move strategically to avoid nested loops — O(n²) → O(n)

Prerequisite: Usually requires sorted data. If unsorted, sort first or use hashing

Opposite direction: left=0, right=end, converge → pair sum, palindrome, container

Same direction: slow=write, fast=read → remove duplicates, move zeroes, partition

Two sequences: One pointer per array → merge sorted, is-subsequence, intersection

3Sum trick: Sort + fix one element + Two Pointers on rest → O(n²)

Dutch Flag: Three pointers (low, mid, high) → 3-way partition in single pass

Move shorter side: In container/water problems, always move the pointer at the shorter wall

Skip duplicates: After finding a match: while (nums[ptr] === nums[ptr±1]) ptr±±

Complexity: Time: O(n) or O(n²) for k-Sum. Space: almost always O(1)

Mental trigger: "Sorted array + pair" or "in-place + O(1) space" → Two Pointers

Decision Flowchart

When to Use Two Pointers — Decision Treetext
Is the data sorted (or can you sort it)?
├── YESDo you need to find a pair/triplet with a property?
│         ├── YESOpposite Direction Two Pointers
│         └── NODo you need to remove/rearrange elements in-place?
│                   ├── YESSame Direction (read/write) Two Pointers
│                   └── NOAre you comparing two sorted sequences?
│                             ├── YESTwo Sequences Two Pointers
│                             └── NOConsider Binary Search or Sliding Window
└── NODo you need original indices?
          ├── YESUse Hash Map instead
          └── NOSort first, then apply Two Pointers

One last thing

Two Pointers is one of the most versatile patterns in DSA. It appears in easy problems (Valid Palindrome) and hard ones (Trapping Rain Water). The difference isn't the technique — it's recognizing WHEN and HOW to apply it. Solve the 7 practice problems in Section 07, and you'll have that intuition locked in.