#125 - Valid Palindrome
July 9, 2025
Valid Palindrome
- Difficulty: Easy
- Topics: Two Pointers, String
- Link: https://leetcode.com/problems/valid-palindrome/
Problem Description
A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers.
Given a string s
, return true
if it is a palindrome, or false
otherwise.
Example 1:
Input: s = "A man, a plan, a canal: Panama"
Output: true
Explanation: "amanaplanacanalpanama" is a palindrome.
Example 2:
Input: s = "race a car"
Output: false
Explanation: "raceacar" is not a palindrome.
Example 3:
Input: s = " "
Output: true
Explanation: s is an empty string "" after removing non-alphanumeric characters.
Since an empty string reads the same forward and backward, it is a palindrome.
Constraints:
1 <= s.length <= 2 * 105
s
consists only of printable ASCII characters.
Solution
1. Problem Deconstruction
Technical Restatement
Given a string s
of printable ASCII characters (length 1 to 200,000), implement a function that:
- Normalizes
s
by:- Converting all alphabetic characters to lowercase
- Removing all non-alphanumeric characters (retain
[a-z0-9]
)
- Returns
true
if the normalized string is a palindrome (reads identically forwards/backwards), elsefalse
.
Beginner-Friendly Explanation
Imagine cleaning up a messy sentence:
- Erase all spaces, punctuation, and symbols (e.g.,
",", ":", " "
) - Convert all letters to lowercase (e.g.,
"A" → "a"
)
Check if the cleaned string is a mirror image of itself (e.g., “radar” reversed is still “radar”).
Mathematical Formulation
Let:
- = input string of length
- be a filtering function:
- (filtered sequence)
- is a palindrome iff:
Constraint Analysis
1 <= s.length <= 2e5
:- Limitation: Rules out O() solutions (e.g., nested loops).
- Edge Case: Single-character strings are trivially palindromic.
- Printable ASCII Characters:
- Limitation: Non-alphanumeric chars (e.g.,
!
,@
) must be skipped efficiently. - Edge Case: Strings with only non-alphanumeric chars (e.g.,
"!@#"
) become empty → valid palindrome.
- Limitation: Non-alphanumeric chars (e.g.,
- Alphanumeric Definition:
- Edge Case: Numbers included (e.g.,
"12321"
is valid). Case sensitivity handled via lowercase conversion.
- Edge Case: Numbers included (e.g.,
2. Intuition Scaffolding
Analogies
- Real-World Metaphor:
- Magazine Cutout Ransom Note: Cut alphanumeric letters, convert to lowercase, arrange on a board. Hold the board up to a mirror—letters should match when viewed normally and in reflection.
- Gaming Analogy:
- Portal Mirror Test: Shoot a “character cleaning ray” that removes non-alphanumeric chars and converts uppercase to lowercase. The ray reflects off a central mirror—if the pre-reflection and post-reflection sequences match, the level is solved.
- Math Analogy:
- Symmetric Matrix Check: Treat the filtered string as a vector . Construct a reflection matrix where if , else . Then is palindromic iff .
Common Pitfalls
- Case Sensitivity Neglect:
- Comparing
'A'
and'a'
as unequal without lowercase conversion.
- Comparing
- Ignoring Non-Alphanumeric Characters:
- Including spaces/punctuation in comparison (e.g., comparing
"a,"
and",a"
).
- Including spaces/punctuation in comparison (e.g., comparing
- Midpoint Miscalculation:
- Forgetting that odd-length palindromes skip the center index.
- Unoptimized Filtering:
- Building a new string with
+=
in a loop (O() time).
- Building a new string with
- Overcleaning:
- Removing alphanumeric chars incorrectly (e.g., deleting digits).
3. Approach Encyclopedia
Approach 1: Brute Force (Two-Pass)
- Filter & Lowercase: Create a new string
clean_s
by iterating throughs
, appending only alphanumeric chars in lowercase. - Palindrome Check: Compare
clean_s
with its reverse.
- Pseudocode:
def isPalindrome(s): clean_chars = [] for char in s: if char.isalnum(): clean_chars.append(char.lower()) clean_s = ''.join(clean_chars) return clean_s == clean_s[::-1]
- Complexity Proof:
- Time: O() for filtering + O() for reversal → O().
- Space: O() for
clean_chars
andclean_s
.
- Visualization:
s = "A man..." Filter: ['a','m','a','n','a','p','l','a','n','a','c','a','n','a','l','p','a','n','a','m','a'] Reverse: ['a','m','a','n','a','p','l','a','n','a','c','a','n','a','l','p','a','n','a','m','a'] → Match!
Approach 2: Optimal (Two-Pointer, Single Pass)
- In-Place Validation: Use left/right pointers starting at both ends. Skip non-alphanumeric chars and compare lowercase equivalents.
- Pseudocode:
def isPalindrome(s): left, right = 0, len(s) - 1 while left < right: while left < right and not s[left].isalnum(): left += 1 while left < right and not s[right].isalnum(): right -= 1 if s[left].lower() != s[right].lower(): return False left += 1 right -= 1 return True
- Complexity Proof:
- Time: O(). Each element visited at most twice (once by each pointer).
- Space: O(). Only two pointers.
- Visualization:
s = "r a c e c a r" Step 1: left='r', right='r' → match Step 2: left='a', right='a' → match Step 3: left='c', right='c' → match Step 4: left='e' (center) → valid palindrome.
4. Code Deep Dive
Optimal Solution (Two-Pointer)
def isPalindrome(s: str) -> bool:
left, right = 0, len(s) - 1 # Initialize pointers at both ends
while left < right: # Continue until pointers meet/cross
# Skip non-alphanumeric chars from left
while left < right and not s[left].isalnum():
left += 1
# Skip non-alphanumeric chars from right
while left < right and not s[right].isalnum():
right -= 1
# Compare lowercase equivalents
if s[left].lower() != s[right].lower():
return False
# Move pointers inward
left += 1
right -= 1
return True # All mirrored pairs matched
Line-by-Line Annotation
- Line 2:
left, right = 0, len(s)-1
- What: Initialize pointers to start and end indices.
- Why: Enables bidirectional traversal for symmetric comparison.
- Line 3:
while left < right:
- What: Loop until pointers meet (even length) or cross (odd length).
- Why: Ensures we only check pairs; center element in odd-length strings is automatically valid.
- Lines 4-6 & 7-9: Skipping non-alphanumeric chars.
- What: Advance
left
until finding an alphanumeric char (similarly forright
). - Why: Avoids comparing irrelevant characters. Inner
while
ensures bounds.
- What: Advance
- Line 10:
if s[left].lower() != s[right].lower():
- What: Compare lowercase alphanumeric chars at current pointers.
- Why: Case-insensitive alphanumeric comparison.
- Lines 11-12: Return
False
on mismatch; else move pointers inward.- Why: Early termination if asymmetry detected.
Edge Case Handling
- Empty/All-Non-Alphanumeric Strings:
- Pointers converge without comparisons → returns
True
(e.g.,s=" "
).
- Pointers converge without comparisons → returns
- Single Character:
left==right
initially → loop skipped →True
.
- Midpoint Handling:
- For odd-length strings, the loop exits when
left==right
, skipping the center.
- For odd-length strings, the loop exits when
5. Complexity War Room
Hardware-Aware Analysis
- Time: O(). At 2e5 elements, ≈ 400k operations (modern CPU: 1e9 ops/sec → negligible).
- Space: O(). 24 bytes for pointers (fits in CPU registers).
Industry Comparison Table
Approach | Time | Space | Readability | Interview Viability |
---|---|---|---|---|
Brute Force | O(n) | O(n) | 9/10 | ✅ (if optimized) |
Two-Pointer | O(n) | O(1) | 8/10 | ✅ Optimal |
Recursive | O(n) | O(n) | 6/10 | ❌ Stack overflow |
6. Pro Mode Extras
Variants
- Case Sensitivity Relaxed:
- If case sensitivity was required, remove
.lower()
and compare raw chars.
- If case sensitivity was required, remove
- k-Ignore Palindrome:
- Problem: Return
true
if removing ≤ k alphanumeric chars makess
palindromic. - Solution: Use two pointers with a mismatch budget.
- Problem: Return
- Longest Palindromic Substring (LC 5):
- Extension: Find the longest substring that is a palindrome.
- Solution: Expand around center or dynamic programming.
Interview Cheat Sheet
- First Mention: “I’ll use a two-pointer approach for O(1) space.”
- Key Insight: “Skipping non-alphanumeric chars in-place avoids extra storage.”
- Test Cases:
- Empty string (
"" → true
) - All non-alphanumeric (
"!@#" → true
) - Mixed case (
"aA" → true
) - Mismatch (
"race a car" → false
).
- Empty string (