Valid Palindrome
Decide whether a string reads the same forwards and backwards, considering only alphanumeric characters and ignoring case.
Open on LeetCode ↗Intuition
The character class is where this one actually breaks. People write the skip as if not s[l].isalpha() and then '0P' comes back True — digits are alphanumeric and must be compared, not skipped, and isalpha silently drops them. Use isalnum. The second half of the trap is building a cleaned copy first: strip, lowercase, compare to the reverse. That is correct, but the filtering does no work the pointers cannot do in place, so it is O(n) space bought for nothing. Keep both pointers on the ORIGINAL string and, before each comparison, walk l forward and r back past anything non-alphanumeric. On 'A man, a plan, a canal: Panama' the commas are never copied anywhere, just stepped over. The invariant is that everything strictly outside l..r has already been verified to mirror.
Two pointers converging, with each side independently skipping non-alphanumeric characters. The continue after each skip is what keeps the logic flat — re-entering the loop re-tests both guards rather than nesting conditions.
Approach
Converge from both ends
Place l at 0 and r at the last index. A palindrome is defined by outer pairs matching, so the natural decomposition is: check the outermost pair, then reduce to the substring strictly inside it. The loop condition l < r is what encodes that reduction; when the pointers meet or cross, every pair has been checked.
Skip non-alphanumerics in place
Before comparing, run two small skip loops: while l < r and s[l] is not alphanumeric, advance l; likewise pull r back. This is the whole trick that removes the cleaned copy. Note both skip loops must keep the l < r guard, otherwise a string of pure punctuation would run a pointer off the end of the string.
Compare case-folded, and fail fast
Lowercase both characters and compare. A single mismatch is a complete counterexample, so return False immediately rather than continuing — there is nothing later in the string that can rescue it. If the loop runs to completion the string is a palindrome, so return True.
Solution & live demo
Common pitfalls
Skipping only one side per iteration
if not s[l].isalnum(): l += 1 if not s[r].isalnum(): r -= 1 if s[l].lower() != s[r].lower(): return False
if not s[l].isalnum():
l += 1
continueWithout the continue, the comparison runs on a character that was just skipped past but not re-validated — a run of two punctuation marks leaves one still in place. Restarting the loop re-checks both guards.
Forgetting to normalise case
if s[l] != s[r]:
if s[l].lower() != s[r].lower():
The problem ignores case, so 'A' and 'a' must compare equal. Raw character comparison rejects most real-world palindrome phrases.
Building a cleaned copy first
t = ''.join(c.lower() for c in s if c.isalnum()) return t == t[::-1]
l, r = 0, len(s) - 1
Correct and readable, but allocates two extra strings. The two-pointer scan answers in O(1) space, which is the version the follow-up asks for.
Edge cases
The skip loops consume everything, the pointers cross without a single comparison, and the function returns True — the empty sequence is trivially a palindrome.
Digits are alphanumeric so they participate; lowercasing '0' is a no-op and 'P' becomes 'p', so '0' vs 'p' correctly fails.
The pointers land on the same index and the loop exits, so the middle character is never compared to anything — which is correct, it mirrors itself.
The l < r guard inside each skip loop stops the pointers from running past each other, and the answer is True.