Reading

Two Pointers: One Search, Two Boundaries

Learn what pointers represent, why ordering makes movement safe, and how two boundaries avoid repeated work.

65 min 4 objectives
Learning objectives
  • Explain what a pointer represents in an array or string
  • Derive inward movement from a palindrome comparison
  • Use sorted order to eliminate impossible pairs
  • State the invariant that makes pointer movement correct

My notes & review

Begin with the search, not the technique

Imagine checking whether a word reads the same from both directions. A direct comparison starts with the first and last characters, then the second and second-to-last characters. You are not doing this because an interview pattern says “use two pointers.” You are doing it because the unanswered part of the word has two meaningful boundaries.

A pointer here is simply a variable containing an index. It does not have the lower-level memory meaning that the word has in languages such as C or C++.

Code
let left = 0
let right = text.length - 1

The two variables describe the part of the input that still needs attention:

Code
R A C E C A R
↑           ↑
left      right

After confirming that the two boundary characters match, move both pointers inward. The unchecked region becomes smaller without revisiting characters that are already settled.

The invariant: what remains true during the loop

An invariant is a statement that remains true every time the loop begins. It is the reason the code can discard processed input safely.

For a palindrome scan, the invariant is:

Everything outside the interval from left through right has already been checked and matches its mirrored character.

Initially, nothing lies outside the interval, so the statement is true. When the boundary characters match, moving inward preserves it. When they differ, the string cannot be a palindrome, so the function can stop immediately.

Code
function isSimplePalindrome(text: string): boolean {
  let left = 0
  let right = text.length - 1

  while (left < right) {
    if (text[left] !== text[right]) return false
    left += 1
    right -= 1
  }

  return true
}

The loop uses left < right, not left <= right, because a middle character has no different partner to compare. An empty string and a one-character string therefore return true without entering the loop.

When the input contains punctuation and mixed case

The comparison rule may say that capitalization and non-alphanumeric characters do not matter. That changes what the pointers are allowed to compare, but it does not change the core method.

At each side:

  1. Skip characters outside the allowed set.
  2. Normalize the two remaining characters to the same case.
  3. Compare them.
  4. If they match, move both boundaries inward.
Code
function isAlphaNumeric(character: string): boolean {
  return /[a-z0-9]/i.test(character)
}

function isPalindrome(text: string): boolean {
  let left = 0
  let right = text.length - 1

  while (left < right) {
    while (left < right && !isAlphaNumeric(text[left])) left += 1
    while (left < right && !isAlphaNumeric(text[right])) right -= 1

    if (text[left].toLowerCase() !== text[right].toLowerCase()) return false

    left += 1
    right -= 1
  }

  return true
}

Trace: "A man, a plan, a canal: Panama"

Code
left sees A, right sees a  → compare a with a, then move both
left skips the space
left sees m, right sees m  → match
right skips punctuation and spaces whenever encountered
...
pointers meet              → every required pair matched

No cleaned copy of the string is required. Each character is visited at most once by a pointer, so the scan takes O(n) time and O(1) auxiliary space.

Would creating a cleaned string be wrong?

No. Normalizing first is often simpler and still takes O(n) time, but it uses O(n) extra space. The two-pointer version demonstrates that the same comparison can be performed without storing a second string.

Sorted order turns comparison into elimination

Two pointers become especially powerful when the input is sorted. Suppose a sorted array contains two numbers whose sum must equal 10:

Code
[1, 2, 4, 7, 9]
 ↑           ↑
left       right

The first pair sums to 10, so it is the answer. Now consider a different target, 8:

Code
1 + 9 = 10, which is too large

Because 9 is the largest remaining value, pairing it with any value to the right of 1 would make an equal or larger sum. Keeping 9 cannot fix the problem. Decrease the sum by moving right leftward.

If the sum were too small, 1 would be the problem: pairing the smallest remaining value with anything below 9 could only make the sum equal or smaller. Increase the sum by moving left rightward.

Code
function twoSumSorted(numbers: number[], target: number): [number, number] | null {
  let left = 0
  let right = numbers.length - 1

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

    if (sum === target) return [left, right]
    if (sum < target) left += 1
    else right -= 1
  }

  return null
}

Worked trace: [2, 3, 4, 6, 9], target 10

Code
left  right  pair   sum  conclusion
2     9      2,9    11   too large; discard 9
2     6      2,6     8   too small; discard 2
3     6      3,6     9   too small; discard 3
4     6      4,6    10   found

Each movement removes an entire row or column of impossible pairs, not merely one pair. That is why the algorithm needs at most n - 1 movements instead of testing roughly n² / 2 combinations.

How to recognize—and reject—the pattern

Two pointers are worth considering when:

  • the answer compares elements at two boundaries;
  • sorted order lets one comparison eliminate many candidates;
  • a contiguous region grows, shrinks, or is partitioned;
  • elements should be processed without allocating another full array.

Do not force converging pointers onto an unsorted pair-sum problem. If the sum is too small in an unsorted array, moving either pointer has no predictable effect. You would first need to sort, use a lookup structure, or find another property that justifies movement.

Complexity vocabulary

Both examples move each pointer in only one direction. Across the whole algorithm, each pointer advances at most n positions, so the work is O(n), not O(n²). A loop inside another loop is not automatically quadratic; what matters is the total number of movements.

The palindrome code uses O(1) auxiliary space. The sorted two-sum scan also uses O(1) extra space when the input is already sorted. Sorting an unsorted input first would usually add O(n log n) time and may affect whether original indices can be returned.

Checkpoint

Before writing a two-pointer loop, answer three questions:

  1. What does each pointer represent?
  2. What remains true outside or inside the pointers?
  3. What fact proves that moving one pointer cannot discard a valid answer?

If the third answer is missing, the movement is a guess rather than an algorithm.