Valid Palindrome is the gateway to the two-pointer pattern. It looks like a string problem, but the real lesson is how to walk a sequence from both ends at once — using O(1) extra space.
Problem. Given a string s, return true if it reads the same forward and backward, considering
only alphanumeric characters and ignoring case. Spaces and punctuation are skipped entirely.
Example: s = "A man, a plan, a canal: Panama" → true. The animation uses the shorter
s = "Race, car", which is also a palindrome once you drop the comma and space.
The slow way first
The obvious idea: build a cleaned-up copy of the string — keep only letters and digits, lowercase them — then reverse it and compare. That works:
clean = [c.lower() for c in s if c.isalnum()]
return clean == clean[::-1]But it allocates two extra strings/lists of size n. We can do the same check with no extra space by comparing characters in place from both ends.
The idea: two pointers closing in
Put a pointer l at the start and a pointer r at the end. A palindrome means s[l] must equal s[r] for every matching pair as we walk inward. So:
- If
s[l]is not alphanumeric, nudgelright and try again. - If
s[r]is not alphanumeric, nudgerleft and try again. - Otherwise both are real characters — compare them lowercased. A mismatch means not a palindrome. A match means step both pointers inward.
When l and r cross without ever finding a mismatch, it is a palindrome.
The key insight: skipping non-alphanumeric characters is just moving one pointer without moving the other. We never copy the string.
Walk through it
Step through the animation. l and r start on the outer "R" and "r", which match. They march inward through "a"/"a" and "c"/"c". Then r lands on a space and a comma — both not alphanumeric — so r slides left twice while l waits. Both pointers meet on "e", match, and cross. No mismatch was ever found, so the answer is True.
Pseudocode
l = 0, r = last index
while l < r:
if s[l] is not alphanumeric: move l right
elif s[r] is not alphanumeric: move r left
elif lowercase(s[l]) != lowercase(s[r]): return False # mismatch
else: move l right and r left # matched pair
return True # crossed with no mismatchThe Python solution
def is_palindrome(s):
l, r = 0, len(s) - 1
while l < r:
if not s[l].isalnum():
l += 1
elif not s[r].isalnum():
r -= 1
elif s[l].lower() != s[r].lower():
return False
else:
l += 1
r -= 1
return Truel, r = 0, len(s) - 1start the two pointers at the ends.- The first two
if/elifbranches skip non-alphanumeric characters — only one pointer moves. s[l].lower() != s[r].lower()is the actual comparison;.lower()makes it case-insensitive.- The final
elseruns only when both characters are real and equal, stepping both inward. - If the loop finishes (pointers crossed) without returning
False, the string is a palindrome.
Complexity
| Case | Time | Notes |
|---|---|---|
| Clean + reverse copy | O(n) (moderate) | but O(n) extra space |
| Two pointers (this solution) | O(n) (moderate) | each char visited once |
O(1) (fast)Every character is looked at a constant number of times as the pointers sweep toward each other, so it is O(n) time. Because we never build a copy, it is O(1) extra space — the win over the clean-and-reverse approach.
When this pattern shows up
Two pointers from opposite ends is the go-to move whenever a problem is about a symmetric property of a sequence or about finding a pair in sorted data: valid palindrome, reverse a string in place, two-sum on a sorted array, container-with-most-water, and trapping rain water.
Do not forget the skip branches. If you only compare and step inward, the space and comma in "Race, car" would be compared against letters and wrongly report a mismatch. Skipping non-alphanumeric characters is the whole point of this problem.
Practice
In 'Race, car', after l and r have matched on R/r, a/a, and c/c, r lands on a space. What happens to l on that step?
1. Why is the two-pointer solution O(1) space while the clean-and-reverse one is O(n)?
2. What happens when s[l] is a non-alphanumeric character?
3. Why call .lower() on both characters before comparing?
4. When does the loop conclude the string IS a palindrome?