AniUI Academy

Two Pointers

Solve array and string problems in one linear pass by walking two positions at once — reversing in place, detecting palindromes, and merging sorted data without extra memory.

10 min read

A lot of array problems look like they need nested loops — compare everything to everything — until you notice the data has a structure a single loop can exploit. Two pointers is the first and most common example: instead of one index walking the array, you track two positions and move them deliberately, and the relationship between them does the work that a nested loop would otherwise need.

Opposite ends: reversing in place

The clearest version: a pointer starting at each end, walking toward the middle.

function reverseInPlace(arr) {
  let left = 0;
  let right = arr.length - 1;
 
  while (left < right) {
    [arr[left], arr[right]] = [arr[right], arr[left]]; // swap
    left++;
    right--;
  }
 
  return arr;
}

Each iteration does exactly one swap and both pointers make guaranteed progress toward each other. The loop runs n/2 times — O(n) time, and crucially O(1) auxiliary space, because there's no second array being built. Compare that to [...arr].reverse()-from-scratch style solutions, which are also O(n) time but O(n) space. Same time complexity, genuinely different space cost — exactly the trade-off from the previous lesson.

Checking a palindrome without checking every pair

A brute-force palindrome check might compare every character to its mirror by recomputing positions repeatedly. Two pointers does it in one pass:

function isPalindrome(str) {
  const clean = str.toLowerCase().replace(/[^a-z0-9]/g, "");
  let left = 0;
  let right = clean.length - 1;
 
  while (left < right) {
    if (clean[left] !== clean[right]) return false;
    left++;
    right--;
  }
 
  return true;
}

This is a genuinely frontend-relevant string problem — validating a user-entered code, a slug, an ID that's supposed to read the same forward and backward — and it's O(n) time, O(n) space (the cleaned copy — though you could avoid even that by skipping non-alphanumeric characters with extra index logic instead of pre-cleaning, trading code clarity for a smaller constant).

Two pointers on sorted data: the classic sum pattern

The opposite-ends pattern gets more powerful when the array is sorted, because moving a pointer has a predictable effect on the sum:

function hasPairWithSum(sortedArr, target) {
  let left = 0;
  let right = sortedArr.length - 1;
 
  while (left < right) {
    const sum = sortedArr[left] + sortedArr[right];
    if (sum === target) return true;
    if (sum < target) left++;   // need a bigger sum — move left up
    else right--;                // need a smaller sum — move right down
  }
 
  return false;
}

Because the array is sorted, moving left rightward can only increase the sum, and moving right leftward can only decrease it. That guarantee is what lets you discard half the remaining possibilities on every step, instead of checking every pair — O(n) time instead of the O(n²) brute-force "check every pair" approach, with O(1) extra space. (The next part of this course covers a hash-map-based version of this same idea, which works even on unsorted data — a useful comparison once you've seen both.)

Same-direction pointers: read and write

The other major shape is two pointers moving the same direction at different speeds — a "slow" pointer marking where the next kept value should go, and a "fast" pointer scanning ahead:

function removeDuplicatesSorted(arr) {
  if (arr.length === 0) return 0;
 
  let writeIndex = 1;
  for (let readIndex = 1; readIndex < arr.length; readIndex++) {
    if (arr[readIndex] !== arr[writeIndex - 1]) {
      arr[writeIndex] = arr[readIndex];
      writeIndex++;
    }
  }
 
  return writeIndex; // new logical length; arr[0..writeIndex) is deduplicated
}

readIndex looks at everything; writeIndex only advances when it finds a genuinely new value, compacting the array in place. This is the exact technique behind the next lesson's broader look at in-place array manipulation, and it's O(n) time, O(1) space — no second array required.

Try it yourself
Loading playground...

What to remember

  • Opposite-ends two pointers turns O(n²) all-pairs comparisons into O(n) — reversing in place, checking palindromes, finding a pair with a target sum on sorted data.
  • Same-direction (read/write) two pointers is how in-place compaction and deduplication work — one pointer scans, the other marks where to write next.
  • The pattern only works because of a guarantee about the data (sortedness, or "kept values are contiguous from the start") — without that guarantee, moving a pointer wouldn't tell you anything useful.
  • In-place two-pointer techniques are usually O(n) time and O(1) auxiliary space, which is the real win over an equivalent-time solution that allocates a new array or object.

Check yourself

4 questions · pass 3/4 to unlock Sliding Window

up to 50
  1. 1.Why does the opposite-ends two-pointer pattern turn an O(n²) brute-force palindrome check into an O(n) one?

  2. 2.In the two-sum-on-a-sorted-array pattern (left pointer at 0, right pointer at the end, looking for a target sum), if arr[left] + arr[right] is less than the target, what should happen?

  3. 3.What is the space complexity of the opposite-ends two-pointer technique for reversing an array in place?

  4. 4.Two pointers moving in the same direction (a "fast and slow" or "read and write" pair) is the pattern behind which of these?

4 left to answer