leetcode-textbook

0125 - Valid Palindrome

Difficulty: Easy Pattern: Two Pointers LeetCode: https://leetcode.com/problems/valid-palindrome/

Concepts used

Problem

Given a string s, return true if it is a palindrome considering only alphanumeric characters and ignoring case, otherwise false. An empty string counts as a palindrome.

Signature:

boolean isPalindrome(String s)

Examples (verbatim from LeetCode):

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

Input:  s = "race a car"
Output: false

Input:  s = " "
Output: true

Intuition

A palindrome is a word that reads the same forwards and backwards – “racecar”, “mom”, “noon”. How would you check by eye? Put one finger on the first letter and one on the last; if they match, slide both fingers one step inward; keep going. If every finger-pair agrees before the fingers meet in the middle, it’s a palindrome. This finger trick is two pointers: one index (left) at the start, one (right) at the end, moving toward each other.

Let’s watch it on the tiniest real case, s = "mom":

This problem adds two twists. First, the string is full of spaces and punctuation (',', ':', ' ') that we must ignore – a comma should never count as a mismatch. Second, uppercase and lowercase count as the same ('A' equals 'a'). The word for “a letter or a digit” is alphanumeric – so 'a' and '7' count, but ' ' and ',' do not.

Here is the general rule with the skipping built in. At each step, if the character under a finger is not alphanumeric, slide just that finger past it and do not compare yet. Once both fingers sit on real letters or digits, lowercase them and compare. If they differ, return false immediately. If they match, move both inward. When the fingers meet or cross with no mismatch, every relevant pair has agreed, so return true. Sweeping through the string once this way is a linear scan; because both fingers only ever move forward, the whole check finishes in O(n) time.

Checkpoint A – The two-finger idea

Pause and pick before expanding. A wrong first guess teaches more than a fast right one.

Q1 (recall). This problem must ignore some characters before comparing. Which set does it keep (the “alphanumeric” ones)?

Show answer **(a)** -- "alphanumeric" means a letter or a digit. Spaces, commas, and colons are skipped; the two `continue` branches advance past them without comparing.

Q2 (comprehend). Trace s = "Aa" (length 2). Both characters are alphanumeric. What is the result, and why?

Show answer **(b)** -- `left=0`, `right=1`. After `toLowerCase`, 'A' becomes 'a' and equals 'a'. Both pointers step inward (`left=1`, `right=0`), `left < right` is now false, and control reaches the final `return true`.

Pseudocode

function isPalindrome(s):
    set left to first index of s
    set right to last index of s
    while left < right:
        if character at left is not alphanumeric:
            advance left
            continue
        if character at right is not alphanumeric:
            move right back one
            continue
        if lowercase(left char) is not equal to lowercase(right char):
            return false
        advance left
        move right back one
    return true

Java Solution

class Solution {
    public boolean isPalindrome(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            char l = s.charAt(left);
            char r = s.charAt(right);
            if (!Character.isLetterOrDigit(l)) {
                left++;
                continue;
            }
            if (!Character.isLetterOrDigit(r)) {
                right--;
                continue;
            }
            if (Character.toLowerCase(l) != Character.toLowerCase(r)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

We snapshot the two characters into l and r first so the two skip-checks read cleanly. Character.isLetterOrDigit matches the problem’s “alphanumeric” exactly (it accepts Unicode letters, which LeetCode accepts too). Comparing after toLowerCase handles the case-insensitive rule. The continue after advancing one pointer restarts the loop so we re-validate the new character before comparing — this is what keeps the skip logic simple.

Complexity

Time:  O(n)  -- each character is visited at most once as left or right advance.
Space: O(1)  -- only two index variables are used; no copy of the string.

Dry-Run

Step-by-step on s = "A man, a plan, a canal: Panama" (length 25, indices 0..24). We show left, right, the characters considered, and the action. M(0) means “match at this step”, SKIP means a character was ignored.

Step left right left char right char Action
1 0 24 ‘A’ ‘a’ (last) match -> left++, right–
2 1 23 ’ ‘ ‘m’ SKIP left (space) -> left++
3 2 23 ‘m’ ‘m’ match -> left++, right–
4 3 22 ‘a’ ‘a’ match -> left++, right–
5 4 21 ‘n’ ‘m’ match? ‘n’ vs ‘a’…

(A full trace is 13 matches; the key point is that every compared pair is equal and the pointers cross without finding a mismatch, so the function returns true.) The loop exits when left >= right, at which point every relevant pair has been verified.

Checkpoint B – Trace and stress it

Q1 (apply). Trace s = "ab". What is returned?

Show answer **(b)** -- both characters are alphanumeric, no skipping happens, and `'a' != 'b'` after lowercasing, so the code hits `return false` on the first comparison.

Q2 (analyze). What happens if a skip branch advances its pointer but you forget the continue (or forget the left++ / right--)?

Show answer **(a)** -- without advancing (or without restarting the loop so the new character is re-checked), progress stalls. This is the exact "infinite loop" mistake called out in Common mistakes below.

Q3 (transfer). Suppose the task changed to: “return true if the string can be a palindrome after deleting at most one character.” How would you adapt the approach?

Show answer Keep the opposite-end two-pointer compare. On the FIRST mismatch, you get one deletion to spend, so try both options -- skip the left character, or skip the right character -- and check whether either remaining substring is a plain palindrome. The skeleton stays; you add a helper that checks a clean palindrome on a substring, and call it twice from the mismatch point.

Common mistakes