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
Suppose we have compared the two outer Rs in RACECAR and moved inward to the As.
We want to continue without checking those Rs again. What lets us forget about them?
We already know they match. After checking the As, we will know the same thing about
two pairs. Each step adds to the part of the answer we have established.
We can describe that knowledge in one sentence that stays true as the pointers move. Such a statement is called an invariant. It explains why the loop can leave processed characters behind without losing something it still needs to check.
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
Our first function compares every character exactly as written. Now suppose the input
is "A, a" and the question says to ignore punctuation, spaces, and letter case. The
expected answer is true: the meaningful characters are just "aa". Comparing the
raw characters would wrongly reject the uppercase A and lowercase a.
How can we apply the same inward scan without constructing a cleaned copy? Before
comparing a pair, move past anything the question says to ignore, then compare both
letters in the same case. In this example, alphanumeric means English letters
a–z or digits 0–9.
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
The palindrome problem tells us exactly which characters to compare: mirrored positions. Now change the task. Given a sorted array and a target, we want two different positions whose values add up to that target. The matching values need not be mirrored, so moving both pointers together would skip possible answers.
We could try every pair. Can sorted order instead tell us which pointer to move after
one comparison? Start with the smallest and largest values and a target of 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
Goal: Find two values at different indices that add up to 10. Follow how each
comparison eliminates one boundary. Here left and right are zero-based indices;
the pair column shows the values at those positions.
left right pair sum conclusion
0 4 2,9 11 too large; discard 9
0 3 2,6 8 too small; discard 2
1 3 3,6 9 too small; discard 3
2 3 4,6 10 found We found values 4 and 6, at zero-based indices 2 and 3. If a particular
problem requests one-based positions, convert these to 3 and 4 when returning.
Each movement rules out all remaining pairs that use the discarded endpoint, not just
the pair we compared.
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
We have used the same two index variables for two different reasons: matching mirrored characters and ruling out impossible sums. Seeing two ends of an array is not enough to justify the technique. When a new problem arrives, ask: what could one comparison tell me about the values I would leave behind?
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
The punctuation-skipping function has small while loops inside another while loop.
Does that mean it repeats a full scan for every character? No: the pointers never
restart. Once a character is passed, that pointer does not visit it again. To measure
the work, count those movements rather than multiplying the visible loops.
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
You should now be able to explain a pointer move without quoting a memorized rule. Choose either the palindrome or sorted-pair example, hide the code, and describe how you would continue from the first comparison.
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.