~20 minutes · Day 8 · two pointers · Python
Interviewers like Valid Palindrome because it looks like a string cleanup problem, but the real test is whether you can keep two positions honest while ignoring noise. You are not trying to memorize a trick. You are learning a small loop shape that appears whenever the two ends of a string have to agree.
Walk one pointer in from each end, skipping what doesn't count, comparing as you go.
That is the whole move. The left pointer starts at the first character, the right pointer starts at the last character, and both move inward. Spaces, punctuation, and symbols do not count for this problem, so the pointers skip them. When both pointers land on letters or digits, compare them in the same case.
The pointers ignore punctuation and spaces, then compare the next real characters.
The problem says to ignore non-alphanumeric characters and compare case-insensitively. Python gives you str.isalnum() for the “does this count?” question and str.lower() for case normalization.
The beginner solution is to build the cleaned string, reverse it, and compare:
def is_palindrome(s):
cleaned = []
for ch in s:
if ch.isalnum(): # keep only letters and digits
cleaned.append(ch.lower()) # compare in one case
cleaned = ''.join(cleaned)
return cleaned == cleaned[::-1] # compare with the reverse
This is clear, and it works. It is O(n) time and O(n) space because the cleaned copy grows with the input. The interview upgrade is to avoid building that copy at all.
Keep the original string in place. Move left until it points at a character that counts. Move right until it points at a character that counts. Then compare.
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1 # skip left-side noise
while left < right and not s[right].isalnum():
right -= 1 # skip right-side noise
if s[left].lower() != s[right].lower():
return False # one mismatch breaks it
left += 1
right -= 1
return True
The reason this stays O(n): neither pointer ever turns around. Every character is skipped, compared, or passed once. The reason it stays O(1) space: the loop carries only two indices.
Because both pointers only move inward, the scan is linear.
Mini-problem: return whether "No 'x' in Nixon" is a palindrome if spaces and punctuation do not count. Before opening the reveal, predict the first two comparisons the pointers should make.
The first comparison is n with n. The next real comparison is o with o. The apostrophes and spaces are skipped because they fail isalnum(). Same invariant, same loop: skip what does not count, compare what remains, then move inward.
| Approach | Time | Space |
|---|---|---|
| Copy, reverse, compare | O(n) | O(n) |
| Two pointers inward | O(n) | O(1) |
The time stays linear either way. The win is that the pointer version does not allocate a cleaned string or its reverse.
1. Which phrase best captures this pattern?
2. Spot the bug in this loop:
while left < right:
while left < right and not s[left].isalnum():
left += 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
3. Which mini-problem fits this same pattern?
4. What is the correct order inside the main loop?
Solve Valid Palindrome in your Notion tracker using the cycle: predict → attempt → compare → encode.
Watch the NeetCode Valid Palindrome solution video at the compare step. Pay attention to the exact moment he decides which characters the pointers are allowed to skip.
Stuck or curious? Ask your teacher (the agent) — that's what it's for.