Valid Palindrome
A phrase reads as a palindrome if — after lowercasing every letter and throwing away every character that is not a letter or digit — the text that remains is the same read left-to-right and right-to-left. Given a string s, return true if it is a palindrome under those rules and false otherwise. Only letters (a–z, A–Z) and digits (0–9) count; spaces, punctuation, and symbols are ignored, and case is not significant. An empty string, or one that becomes empty after the non-alphanumeric characters are removed, counts as a palindrome — return true.
- 1 <= s.length <= 2 × 10⁵
- s consists of printable ASCII characters
- Non-alphanumeric characters are ignored
- Comparison is case-insensitive
s = "A man, a plan, a canal: Panama"trues = "race a car"falses = " "trues = "0P"falseWe want to know whether a phrase reads the same forwards and backwards — a palindrome. The catch is that real text is messy. "A man, a plan, a canal: Panama" is a palindrome, but only once you ignore the spaces, the comma, and the colon, and stop caring whether a letter is a capital 'A' or a small 'a'. So the rule is: look only at letters and digits, treat upper and lower case as the same, and ask whether that cleaned-up sequence is a mirror of itself.
The first idea most people have is to do the cleanup literally. Walk the string once, keep only the letters and digits, lowercase them, and build a brand-new clean string. Then make a second string that is the clean one reversed, and check whether the two are equal.
cleaned = [c.lower() for c in s if c.isalnum()] # keep only letters/digits, lowercased
return cleaned == cleaned[::-1] # is it the same reversed?This works, and it runs in O(N) time — we touch each character a fixed number of times. But look at the memory. We built a whole second copy of the string, and then a reversed copy on top of that. For a string of length N that is O(N) extra space. And it is a little silly: to check a mirror, we made a copy just so we could read it from both directions — when we could already read the original from both directions. A string lets you jump straight to any position by its index, so index 0 is the first character and the last index is the final one. Why copy at all?
Keep two position markers. Call one left, starting at index 0 (the first character), and one right, starting at the last index (the final character). A palindrome means the first character matches the last, the second matches the second-to-last, and so on working inward. So compare the characters under left and right. If they are equal, that pair is fine — step left one to the right and right one to the left, and check the next pair further in. If they ever disagree, the phrase is not a palindrome and we stop right there and return false. The noise is handled as we go: if left is sitting on something that is not a letter or digit, just push left inward one step without comparing; same for right. We never build a second string — we walk the original from both ends toward the middle.
left, right = 0, len(s) - 1
while left < right:
if not s[left].isalnum(): # left is on noise — skip it
left += 1
elif not s[right].isalnum(): # right is on noise — skip it
right -= 1
else: # both on real characters — compare
if s[left].lower() != s[right].lower():
return False # a mismatched pair: not a palindrome
left += 1
right -= 1
return True # the two markers met with no mismatchleft < right. The moment they meet or cross, every pair has already been checked, so we are done — return true. A middle character in an odd-length string never needs comparing, because it is its own mirror. And notice we compare only when both markers are on real characters: the two skip branches make sure a space on one side is never lined up against a letter on the other.Here is the quiet reason this works. Every time a pair matches, those two characters have done their whole job — they can never be part of a future mismatch, so we throw them away and never look at them again. What is left is a smaller string in the middle, and the original is a palindrome exactly when that smaller middle is. We keep chopping one character off each end until nothing is left. The fact that stays true the entire time — call it the invariant — is this: everything outside the left…right window has already been confirmed to mirror correctly. When the window empties, the whole string is confirmed.
left=0 ('A'), right=29 ('a'). Both real. 'a' == 'a' — match. Move both inward.left=1 (' '). A space, not a letter or digit — skip it, left=2 ('m'). right is on 'm'. Match. Move both.left and right cross in the middle with no mismatch ever found. Return true.Now a failing case, s = "race a car". Cleaned it is "raceacar": 'r'/'r' match, 'a'/'a' match, 'c'/'c' match, then left is on 'e' and right is on 'a' — a mismatch. Return false immediately, without bothering to check the rest.
Two markers that start at opposite ends and walk toward each other, each step letting us permanently discard at least one end — that is the converging form of the two-pointer pattern. (Its cousin, the same-direction form from Move Zeroes and Is Subsequence, had both pointers walking the same way; here they come at the problem from opposite sides.) It turns work that looks like it needs O(N²) — compare everything against everything — into a single O(N) sweep, because across the whole run each pointer only ever moves inward, so together they take about N steps in total.
Converging only works when the input has a property that lets the two ends tell you something. For a palindrome, that property is symmetry: the ends are supposed to be mirror images, so a matched pair is provably finished and a mismatched pair is a provable answer. In the sorted-array problems coming next, the property is order instead, and it will let you decide which one of the two ends to throw away. Either way, the reusable skeleton is the same:
left, right = 0, n - 1
while left < right:
look at the pair at (left, right)
from that pair, decide which end to discard: move left in, move right in, or both
maybe record somethingSolving a new converging problem is really just filling three blanks: (a) what you read from the two ends, (b) which pointer that tells you to move, and (c) what you record along the way. For Valid Palindrome: (a) are the two characters equal? (b) on a match move both inward, on a mismatch stop; (c) nothing to record — just answer true or false.
Reach for converging two pointers when you see: a check for symmetry or a palindrome; or a sorted array with a question about a pair (or triplet) whose values hit a target; or two boundaries — walls, the two ends of an array — where the answer lives between them. A limit like N up to 10⁵ is a hint too: it rules out an O(N²) double loop, so the interviewer is expecting a linear sweep, and coming in from both ends is one of the few ways to get there. The trap to keep in mind: the sorted-array version needs the sorting. If an array is unsorted, comparing its two ends tells you nothing about which side to drop, and the pattern falls apart — you would sort first (if you are allowed) or reach for a hash map instead. Train the reflex: whenever the useful information lives at the two ends of an ordered or symmetric thing, try walking inward from both.
Valid Palindrome Visualization
Checking symmetry while ignoring symbols
Key Insight
We can skip non-alphanumeric characters by iterating the pointers past them before performing the comparison.
Pointers Strategy
Converge from both ends. If characters (lower-cased) don't match, return false immediately. If they meet, return true.