DSATwo Pointers Technique

Two Pointers Technique

The two pointers technique is one of the most practical tools in competitive programming and coding interviews. Instead of using a nested loop (O(n²)) to check every pair of elements, you maintain two index variables that move through the data in a coordinated way — cutting the work down to O(n).

The pattern appears in dozens of classic problems: finding pairs in sorted arrays, checking palindromes, removing duplicates, partitioning arrays, and more. Once you recognise the shape, solutions become straightforward.

The two core patterns

Every two-pointer problem falls into one of two shapes:

1. Opposite-end pointers — one pointer starts at the left, one at the right. They move toward each other until they meet. Best for problems where you want to process from both ends of a sorted array simultaneously.

2. Same-direction pointers (fast / slow) — both pointers start at the left, but one runs ahead of the other. Used when you want to maintain a condition about a window or partition as you scan forward.

Template 1: Opposite-end pointers

JS
function twoPointerOpposite(arr) {
  let left = 0;
  let right = arr.length - 1;

  while (left < right) {
    // Process arr[left] and arr[right]
    if (conditionMet(arr, left, right)) {
      return [left, right];
    } else if (shouldMoveLeft(arr, left, right)) {
      left++;
    } else {
      right--;
    }
  }

  return null; // not found
}

Template 2: Same-direction (fast / slow) pointers

JS
function twoPointerSameDirection(arr) {
  let slow = 0; // "write" pointer — next valid position

  for (let fast = 0; fast < arr.length; fast++) {
    if (isValid(arr[fast])) {
      arr[slow] = arr[fast]; // write valid value
      slow++;
    }
  }

  return slow; // length of valid prefix
}
When to use two pointers
  • Sorted array, find a pair / triplet with target sum. Sorting + opposite pointers beats O(n²) brute force.

  • Palindrome or symmetry check. Compare characters from both ends inward.

  • Remove duplicates or filter in-place. Fast pointer scans, slow pointer writes.

  • Partition arrays. Dutch National Flag, moving zeroes, etc.

  • Merge two sorted arrays. Two pointers, one per array, advance the smaller.

  • Cycle detection in linked lists. Floyd's hare-and-tortoise algorithm.

Two pointers vs. sliding window
Two pointers and sliding window are cousins. Two pointers usually refers to opposite-end or fast/slow patterns on arrays. Sliding window refers to a variable-size contiguous subarray maintained from the left. When the pointers always move in the same direction and the window must be contiguous, that is typically called sliding window.
Problem 1 — Two Sum II (sorted array)

Given a 1-indexed sorted array and a target, return the indices of the two numbers that add up to the target. Exactly one solution exists.

Leetcode 167

Brute force: O(n²) — check every pair.

Two pointers: O(n) — start at both ends, advance left when the sum is too small, retreat right when too large.

Two Sum II — O(n) time, O(1) space

JS
function twoSum(numbers, target) {
  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]; // 1-indexed answer
    } else if (sum < target) {
      left++;  // need a larger value
    } else {
      right--; // need a smaller value
    }
  }

  return []; // guaranteed to find answer per problem constraints
}

// Examples
twoSum([2, 7, 11, 15], 9); // [1, 2]  (2 + 7 = 9)
twoSum([2, 3, 4],      6); // [1, 3]  (2 + 4 = 6)

Why it works: the array is sorted. If the sum is too small, the only way to increase it is to move left right (pick a bigger left value). If too large, move right left. Each step eliminates at least one element from consideration, so the loop runs at most n times.

Problem 2 — Valid Palindrome

Given a string, determine if it reads the same forwards and backwards after keeping only alphanumeric characters and lowercasing everything.

Leetcode 125

Opposite-end pointers are perfect: compare the outermost characters and move inward. If they differ at any point, it is not a palindrome.

Valid Palindrome — O(n) time, O(1) space

JS
function isPalindrome(s) {
  const isAlphanumeric = (c) => /[a-z0-9]/.test(c.toLowerCase());

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

  while (left < right) {
    // Skip non-alphanumeric from both ends
    while (left < right && !isAlphanumeric(s[left])) {
      left++;
    }
    while (left < right && !isAlphanumeric(s[right])) {
      right--;
    }

    if (s[left].toLowerCase() !== s[right].toLowerCase()) {
      return false;
    }

    left++;
    right--;
  }

  return true;
}

// Examples
isPalindrome("A man, a plan, a canal: Panama"); // true
isPalindrome("race a car");                     // false
isPalindrome(" ");                              // true (empty after filter)
Tip
The inner while loops skip non-alphanumeric characters. Always guard them with left < right to avoid the pointers crossing while skipping — that would make you compare the same character against itself.
Problem 3 — Container With Most Water

Given an integer array height of length n, find two lines that together with the x-axis form a container that holds the most water.

Leetcode 11

The volume of water between lines at index i and j is:

water = (j - i) * Math.min(height[i], height[j])

Greedy insight: start with the widest container (left = 0, right = n-1). Move the pointer at the shorter line inward — there is no point keeping the shorter line because it is already the bottleneck. Moving the taller line inward can only make things worse (width shrinks AND height stays bounded by the shorter line).

Container With Most Water — O(n) time, O(1) space

JS
function maxArea(height) {
  let left = 0;
  let right = height.length - 1;
  let maxWater = 0;

  while (left < right) {
    const width = right - left;
    const currentWater = width * Math.min(height[left], height[right]);
    maxWater = Math.max(maxWater, currentWater);

    // Move the shorter wall inward
    if (height[left] <= height[right]) {
      left++;
    } else {
      right--;
    }
  }

  return maxWater;
}

// Example
maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7]); // 49  (between index 1 and 8)
Problem 4 — 3Sum

Find all unique triplets in the array that sum to zero.

Leetcode 15

Strategy: sort the array, then for each element nums[i] run a two-pointer search on the subarray to the right. This gives O(n²) overall — far better than the O(n³) brute force.

Handling duplicates is the tricky part: skip duplicate values of i and also skip duplicate inner pointer values after finding a valid triplet.

3Sum — O(n²) time, O(1) extra space (ignoring output)

JS
function threeSum(nums) {
  nums.sort((a, b) => a - b);
  const result = [];

  for (let i = 0; i < nums.length - 2; i++) {
    // Skip duplicate values for the outer pointer
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    // Early exit: smallest possible sum is already positive
    if (nums[i] > 0) break;

    let left = i + 1;
    let right = nums.length - 1;

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

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

        // Skip duplicates for left and 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 result;
}

// Examples
threeSum([-1, 0, 1, 2, -1, -4]); // [[-1,-1,2],[-1,0,1]]
threeSum([0, 0, 0]);              // [[0,0,0]]
threeSum([1, 2, -2, -1]);        // []
Warning
Always sort before running the two-pointer inner loop. Without sorting, the greedy movement logic (move left if sum is too small) is invalid because smaller indices do not imply smaller values.
Problem 5 — Remove Duplicates from Sorted Array

Given a sorted array, remove duplicates in-place and return the length of the unique prefix. The relative order of elements must be preserved.

Leetcode 26

This is the classic fast/slow pattern. The slow pointer tracks where the next unique value should go; the fast pointer scans forward looking for a value different from the one just written.

Remove Duplicates — O(n) time, O(1) space

JS
function removeDuplicates(nums) {
  if (nums.length === 0) return 0;

  let slow = 1; // next write position (index 0 is always kept)

  for (let fast = 1; fast < nums.length; fast++) {
    if (nums[fast] !== nums[fast - 1]) {
      // New unique value found — write it at the slow position
      nums[slow] = nums[fast];
      slow++;
    }
  }

  return slow; // length of unique prefix
}

// Example
const arr = [1, 1, 2, 3, 3, 3, 4];
const k = removeDuplicates(arr);
console.log(k);               // 4
console.log(arr.slice(0, k)); // [1, 2, 3, 4]
Variant: allow at most k duplicates
For "remove duplicates so each value appears at most twice" (Leetcode 80), compare nums[fast] !== nums[slow - 2] instead of nums[fast] !== nums[fast - 1]. Generalize to at most k copies: compare nums[fast] !== nums[slow - k].
Problem 6 — Dutch National Flag (Sort Colors)

Given an array of 0s, 1s, and 2s, sort it in-place in a single pass without using the built-in sort.

Leetcode 75

This classic problem by Dijkstra uses three pointers — a natural extension of two pointers. Maintain four regions at all times:

  • [0, low-1] — all 0s
  • [low, mid-1] — all 1s
  • [high+1, n-1] — all 2s
  • [mid, high] — unexplored

The mid pointer scans forward. When it hits a 0, swap with low and advance both. When it hits a 2, swap with high and retreat high only (the swapped-in value is unexplored). When it hits a 1, just advance.

Sort Colors (Dutch National Flag) — O(n) time, O(1) space

JS
function sortColors(nums) {
  let low = 0;
  let mid = 0;
  let high = nums.length - 1;

  const swap = (i, j) => {
    [nums[i], nums[j]] = [nums[j], nums[i]];
  };

  while (mid <= high) {
    if (nums[mid] === 0) {
      swap(low, mid);
      low++;
      mid++;
      // nums[low] was a 1 (already in 1s region), safe to advance mid
    } else if (nums[mid] === 2) {
      swap(mid, high);
      high--;
      // Do NOT advance mid — the swapped-in value is unexplored
    } else {
      mid++; // nums[mid] === 1, already in the right region
    }
  }
}

// Example
const colors = [2, 0, 2, 1, 1, 0];
sortColors(colors);
console.log(colors); // [0, 0, 1, 1, 2, 2]
Tip
Why not advance mid after swapping with high? When you swap nums[mid] with nums[high], the value now at nums[mid] came from the unexplored region — it could be a 0, 1, or 2. You must examine it before advancing.
Complexity summary

Problem

Time

Space

Pattern

Two Sum II

O(n)

O(1)

Opposite-end

Valid Palindrome

O(n)

O(1)

Opposite-end

Container With Most Water

O(n)

O(1)

Opposite-end (greedy)

3Sum

O(n²)

O(1)*

Sort + opposite-end inner

Remove Duplicates

O(n)

O(1)

Fast / slow

Sort Colors

O(n)

O(1)

Three-pointer partition

*O(k) extra space for the output list in 3Sum, where k is the number of triplets found. The algorithm itself uses O(1) extra space.

When NOT to use two pointers
  • Unsorted array, arbitrary pair constraints. Sorting costs O(n log n); if that is too slow, a hash map is usually faster.

  • Non-contiguous access patterns. Two pointers work because adjacent values are meaningful. Random-access problems need different approaches.

  • You need all pairs, not just one. Two pointers finds one optimal or valid pair efficiently. Enumerating all pairs is inherently O(n²).

  • Linked lists with arbitrary jumps. The fast/slow pattern works on linked lists for cycle detection, but random index access is not possible.

Common mistakes and edge cases
  1. Off-by-one on termination. Use left < right (strict), not left <= right. When they meet on the same element you have nothing to compare.

  2. Forgetting to skip duplicates in 3Sum. Without the duplicate-skipping inner loops, you push the same triplet multiple times.

  3. Moving the wrong pointer in Container With Most Water. Always move the pointer at the shorter wall — moving the taller one can never increase the area.

  4. Not advancing mid after swapping with low in Dutch National Flag. When you swap a 0 to the front, the value that came from low was already processed (a 1), so mid can safely advance.

  5. Array of length 0 or 1. Most two-pointer problems return trivially for tiny inputs; add the guard at the top.

  6. Sorted order assumption violated. If you sort in a variant that needs the original indices, stash the indices as tuples before sorting.

Recognising the pattern in interviews
When a problem says "sorted array" and asks you to find a pair or triplet with a property — reach for two pointers first. When a problem says "in-place" on a sorted array — reach for the fast/slow pattern. These two phrases are the strongest signals.