Valid Palindrome
Converging Two Pointers
Converging pointers from both ends. Simple but builds the muscle memory.
Problem
Given a string s, return true if it is a palindrome, or false otherwise.
You must ignore non-alphanumeric characters and treat uppercase and lowercase letters as equivalent.
Intuition
A palindrome reads the same from the left and the right. That symmetry is exactly what two pointers are good at.
The only complication is the filtering rule. We cannot compare every character directly because punctuation and spaces are ignored, and letters are case-insensitive. So the pointers must first skip over irrelevant characters, then compare the normalized values.
This is the canonical "converging pointers" pattern: start at both ends, move inward, and stop immediately when a mismatch appears.
Approach
Converging Two Pointers
O(n)O(1)Keep one pointer at the left end and one at the right end.
- Advance
leftuntil it lands on an alphanumeric character. - Advance
rightbackward until it lands on an alphanumeric character. - Compare the lowercase versions of the two characters.
- If they differ, the string is not a palindrome.
- Otherwise, move both pointers inward and continue.
Every character is examined at most once by each pointer, so the scan is linear.
impl Solution {
pub fn is_palindrome(s: String) -> bool {
let bytes = s.as_bytes();
let mut left = 0usize;
let mut right = bytes.len().saturating_sub(1);
while left < right {
while left < right && !bytes[left].is_ascii_alphanumeric() {
left += 1;
}
while left < right && !bytes[right].is_ascii_alphanumeric() {
right -= 1;
}
if bytes[left].to_ascii_lowercase() != bytes[right].to_ascii_lowercase() {
return false;
}
left += 1;
right -= 1;
}
true
}
}function isPalindrome(s: string): boolean {
const isAlphaNum = (code: number): boolean =>
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122);
let left = 0;
let right = s.length - 1;
while (left < right) {
while (left < right && !isAlphaNum(s.charCodeAt(left))) left++;
while (left < right && !isAlphaNum(s.charCodeAt(right))) right--;
if (s[left].toLowerCase() !== s[right].toLowerCase()) return false;
left++;
right--;
}
return true;
}The invariant is that everything outside the pointers has already been validated. If the current pair matches, they can be discarded permanently.
Trace
For s = "A man, a plan, a canal: Panama":
| left | right | chars | action |
|---|---|---|---|
| 0 | 29 | A / a | match |
| 1 | 28 | m / m | match |
| 2 | 27 | a / a | match |
| ... | ... | ... | skip spaces and punctuation as needed |
The important part is not each character, but that the pointers only move inward.
Edge cases
- Empty string is a palindrome.
- Only punctuation is also a palindrome after filtering.
- Single alphanumeric character is trivially a palindrome.
- Mixed case should normalize before comparison.
If you can state the answer as "the left side must mirror the right side," two pointers are usually the right first move.
Takeaway
Two pointers are a symmetry tool.
When the correctness condition depends on the ends of a sequence matching each other, you can often solve the problem in one pass by converging from both sides and skipping anything irrelevant along the way.
Why does moving the left pointer increase the sum? Understand the proof.
Your first sliding window. When do you shrink vs expand?
The ‘hard’ sliding window — but if you understood #3, this is the same pattern with a counter.
More in Two Pointers & Sliding Window
Why does moving the left pointer increase the sum? Understand the proof.
Fix one element, two-pointer the rest. Duplicate handling is the real challenge.
Your first sliding window. When do you shrink vs expand?
The ‘hard’ sliding window — but if you understood #3, this is the same pattern with a counter.