Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9.11 Valid Palindrome

Source: src/main/kotlin/string/ValidPalindrome.kt (+ string/ValidPalindrome_II.kt) Pattern: two pointers with a filter predicate · Core page

The Problem

Given a string, determine if it is a palindrome considering only alphanumeric characters and ignoring case.

  • Constraints: $1 \le n \le 2 \times 10^5$; printable ASCII.

Examples

Input:  s = "A man, a plan, a canal: Panama"   -> Output: true
Input:  s = "race a car"                       -> Output: false

Intuition — two pointers that skip non-alphanumerics

The 3.1 two-pointer shape, with the “advance” rules extended: on each side, skip any character that isn’t a letter or digit, then compare the survivors case-insensitively. The repo’s isAlpha predicate drives a 4-way when:

left = 0, right = last
while left < right:
    if both alphanumeric:
        if they differ (case-insensitively): return false
        left++; right--
    else if right is not alphanumeric: right--      # skip the invalid one
    else if left is not alphanumeric: left++        # skip the invalid one
    else: left++; right--
return true

Why skip before comparing? The valid characters are the only ones that must mirror — punctuation and spaces are noise. The two-pointer skip is the 9.6 “filter as you go” discipline, avoiding a pre-filter pass.

Why case-insensitive? 'A' and 'a' are the same letter for palindrome purposes — the lowercaseChar() comparison normalizes both sides. The repo’s isAlpha uses Character.isAlphabetic || Character.isDigit — alphanumeric means letters and digits ("0P" is not a palindrome: 0 ≠ p).

Approach 1 — Filter, reverse, compare (O(n) space)

filter(isAlphanumeric).lowercase() then compare with its reverse: simple, but builds a second string.

Approach 2 — Two-pointer with skip (the repo’s version, optimal)

class ValidPalindrome {
    /**
     * @param s input string
     * @return  true iff the alphanumerics mirror ignoring case
     */
    fun isPalindrome(s: String): Boolean {
        var left = 0
        var right = s.lastIndex
        val isAlpha = { ch: Char -> Character.isAlphabetic(ch.code) || Character.isDigit(ch.code) }

        while (left < right) {
            when {
                // Both are valid characters: compare them
                isAlpha(s[left]) && isAlpha(s[right])
                        && s[left].lowercaseChar() != s[right].lowercaseChar() -> return false

                // One side is invalid: skip it
                isAlpha(s[left]) && !isAlpha(s[right]) -> right--
                isAlpha(s[right]) && !isAlpha(s[left]) -> left++

                // Both valid and equal: move inward
                else -> {
                    left++
                    right--
                }
            }
        }
        return true
    }
}
public class ValidPalindrome {
    /**
     * @param s input string
     * @return  true iff the alphanumerics mirror ignoring case
     */
    public boolean isPalindrome(String s) {
        int left = 0, right = s.length() - 1;

        while (left < right) {
            char l = s.charAt(left), r = s.charAt(right);
            if (!Character.isLetterOrDigit(l)) { left++; }          // skip invalid
            else if (!Character.isLetterOrDigit(r)) { right--; }    // skip invalid
            else if (Character.toLowerCase(l) != Character.toLowerCase(r)) {
                return false;                                       // mismatch
            } else {
                left++; right--;
            }
        }
        return true;
    }
}
#include <string>
#include <cctype>

class ValidPalindrome {
public:
    /**
     * @param s input string
     * @return  true iff the alphanumerics mirror ignoring case
     */
    bool isPalindrome(std::string s) {
        int left = 0, right = s.size() - 1;

        while (left < right) {
            if (!std::isalnum(s[left])) { left++; }                 // skip invalid
            else if (!std::isalnum(s[right])) { right--; }          // skip invalid
            else if (std::tolower(s[left]) != std::tolower(s[right])) {
                return false;                                       // mismatch
            } else {
                left++; right--;
            }
        }
        return true;
    }
};
def is_palindrome(s: str) -> bool:
    """
    @param s: input string
    @return:  true iff the alphanumerics mirror ignoring case
    """
    left, right = 0, len(s) - 1

    while left < right:
        if not s[left].isalnum():
            left += 1                     # skip invalid
        elif not s[right].isalnum():
            right -= 1                    # skip invalid
        elif s[left].lower() != s[right].lower():
            return False                  # mismatch
        else:
            left += 1
            right -= 1
    return True
#![allow(unused)]
fn main() {
impl Solution {
    /// @param s input string
    /// @return  true iff the alphanumerics mirror ignoring case
    pub fn is_palindrome(s: String) -> bool {
        let chars: Vec<char> = s.chars().collect();
        let (mut left, mut right) = (0usize, chars.len().saturating_sub(1));

        while left < right {
            if !chars[left].is_alphanumeric() { left += 1; }          // skip invalid
            else if !chars[right].is_alphanumeric() { right -= 1; }   // skip invalid
            else if chars[left].to_lowercase().next() != chars[right].to_lowercase().next() {
                return false;                                         // mismatch
            } else {
                left += 1;
                right -= 1;
            }
        }
        true
    }
}
}

Dry run

Input: s = "A man, a plan, a canal: Panama".

left=0 'A', right=23 'a'  -> both alnum; 'a' == 'a' -> move in.
left=1 ' ', right=22 'm'  -> left is not alnum -> left++ (skip the space).
left=2 'm', right=22 'm'  -> equal -> move in.
left=3 'a', right=21 'a'  -> equal.
left=4 'n', right=20 'n'  -> equal.
... (the punctuation gets skipped on whichever side holds it)
left=12 'c', right=12 'c' -> equal -> left=13, right=11 -> loop ends.

Output: true ✓

The skip rules interleave naturally: when left hits a space or comma, it advances alone; when right does, it retreats alone; only when both sides hold alphanumerics is there a comparison. "race a car" fails at the 'e' vs 'c' comparison (race vs rac are not mirrors).

Complexity

Time. One pass, O(1) per character:

$$ T(n) = O(n) $$

Space. Two pointers:

$$ S(n) = O(1) $$

Variants & follow-ups

  • Valid Palindrome II (string/ValidPalindrome_II.kt) — one deletion allowed: on the first mismatch, check the two substrings skipping either side.
  • Longest Palindromic Substring (9.4) — the expand-around-center upgrade of the same mirror idea.
  • Reverse Vowels Of A String (string/ReverseVowelOfString.kt) — two pointers with a vowel predicate: the same skip-and-swap rhythm.
  • Interview follow-up: “Why skip in both directions instead of pre-filtering?” Pre-filtering copies the string (O(n) space). The two-pointer skip does the filtering during the comparison — the same O(n) time with O(1) space, and the case-normalization happens at the comparison site.