← ~/lc-grind/posts

#125 - Valid Palindrome

July 9, 2025

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:

  1. Normalizes s by:
    • Converting all alphabetic characters to lowercase
    • Removing all non-alphanumeric characters (retain [a-z0-9])
  2. Returns true if the normalized string is a palindrome (reads identically forwards/backwards), else false.

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:

  • ss = input string of length nn
  • f:charchar{}f: \text{char} \rightarrow \text{char} \cup \{\emptyset\} be a filtering function:

    f(c)={lowercase(c)if c is alphanumericotherwisef(c) = \begin{cases} \text{lowercase}(c) & \text{if } c \text{ is alphanumeric} \\ \emptyset & \text{otherwise} \end{cases}

  • t=[f(s0),f(s1),,f(sn1)] without t = [f(s_0), f(s_1), \dots, f(s_{n-1})] \text{ without } \emptyset (filtered sequence)
  • tt is a palindrome iff:

    i[0,t2),ti=tt1i\forall i \in \left[0, \left\lfloor \frac{|t|}{2} \right\rfloor \right), t_i = t_{|t|-1-i}

Constraint Analysis

  1. 1 <= s.length <= 2e5:
    • Limitation: Rules out O(n2n^2) solutions (e.g., nested loops).
    • Edge Case: Single-character strings are trivially palindromic.
  2. 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.
  3. Alphanumeric Definition:
    • Edge Case: Numbers included (e.g., "12321" is valid). Case sensitivity handled via lowercase conversion.

2. Intuition Scaffolding

Analogies

  1. 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.
  2. 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.
  3. Math Analogy:
    • Symmetric Matrix Check: Treat the filtered string as a vector t\mathbf{t}. Construct a reflection matrix R\mathbf{R} where Rij=1R_{ij} = 1 if i+j=t1i + j = |\mathbf{t}|-1, else 00. Then t\mathbf{t} is palindromic iff Rt=t\mathbf{R} \mathbf{t} = \mathbf{t}.

Common Pitfalls

  1. Case Sensitivity Neglect:
    • Comparing 'A' and 'a' as unequal without lowercase conversion.
  2. Ignoring Non-Alphanumeric Characters:
    • Including spaces/punctuation in comparison (e.g., comparing "a," and ",a").
  3. Midpoint Miscalculation:
    • Forgetting that odd-length palindromes skip the center index.
  4. Unoptimized Filtering:
    • Building a new string with += in a loop (O(n2n^2) time).
  5. Overcleaning:
    • Removing alphanumeric chars incorrectly (e.g., deleting digits).

3. Approach Encyclopedia

Approach 1: Brute Force (Two-Pass)

  1. Filter & Lowercase: Create a new string clean_s by iterating through s, appending only alphanumeric chars in lowercase.
  2. 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(nn) for filtering + O(nn) for reversal → O(nn).
    • Space: O(nn) for clean_chars and clean_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)

  1. 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(nn). Each element visited at most twice (once by each pointer).
    • Space: O(11). 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

  1. Line 2: left, right = 0, len(s)-1
    • What: Initialize pointers to start and end indices.
    • Why: Enables bidirectional traversal for symmetric comparison.
  2. 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.
  3. Lines 4-6 & 7-9: Skipping non-alphanumeric chars.
    • What: Advance left until finding an alphanumeric char (similarly for right).
    • Why: Avoids comparing irrelevant characters. Inner while ensures bounds.
  4. Line 10: if s[left].lower() != s[right].lower():
    • What: Compare lowercase alphanumeric chars at current pointers.
    • Why: Case-insensitive alphanumeric comparison.
  5. 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=" ").
  • Single Character:
    • left==right initially → loop skipped → True.
  • Midpoint Handling:
    • For odd-length strings, the loop exits when left==right, skipping the center.

5. Complexity War Room

Hardware-Aware Analysis

  • Time: O(nn). At 2e5 elements, ≈ 400k operations (modern CPU: 1e9 ops/sec → negligible).
  • Space: O(11). 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

  1. Case Sensitivity Relaxed:
    • If case sensitivity was required, remove .lower() and compare raw chars.
  2. k-Ignore Palindrome:
    • Problem: Return true if removing ≤ k alphanumeric chars makes s palindromic.
    • Solution: Use two pointers with a mismatch budget.
  3. 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:
    1. Empty string ("" → true)
    2. All non-alphanumeric ("!@#" → true)
    3. Mixed case ("aA" → true)
    4. Mismatch ("race a car" → false).