Reading
Two Pointers: One Search, Two Boundaries
Learn what pointers represent, why ordering makes movement safe, and how two boundaries avoid repeated work.
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
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++.
let left = 0
let right = text.length - 1 The two variables describe the part of the input that still needs attention:
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
leftthroughrighthas 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.
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:
- Skip characters outside the allowed set.
- Normalize the two remaining characters to the same case.
- Compare them.
- If they match, move both boundaries inward.
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"
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:
[1, 2, 4, 7, 9]
↑ ↑
left right The first pair sums to 10, so it is the answer. Now consider a different target, 8:
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.
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
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:
- What does each pointer represent?
- What remains true outside or inside the pointers?
- 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.