LeetCode 125 - Valid Palindrome
This problem asks us to determine whether a given string is a palindrome after applying two transformations: 1. Convert all uppercase letters to lowercase. 2. Remove all non-alphanumeric characters. An alphanumeric character is any English letter (a-z, A-Z) or digit (0-9).
Difficulty: 🟢 Easy
Topics: Two Pointers, String
Solution
Problem Understanding
This problem asks us to determine whether a given string is a palindrome after applying two transformations:
- Convert all uppercase letters to lowercase.
- Remove all non-alphanumeric characters.
An alphanumeric character is any English letter (a-z, A-Z) or digit (0-9). Characters such as spaces, punctuation marks, commas, colons, and symbols should be ignored completely.
After preprocessing, we must check whether the resulting string reads the same forward and backward. If it does, we return true; otherwise, we return false.
For example, the input string:
"A man, a plan, a canal: Panama"
becomes:
"amanaplanacanalpanama"
Since this string reads the same in both directions, the answer is true.
The constraints are important:
- The string length can be as large as
2 * 10^5 - The input consists only of printable ASCII characters
Because the input can be quite large, an inefficient solution may become too slow or consume unnecessary memory. We should aim for a linear time solution.
Several edge cases are worth identifying upfront. The string may contain only punctuation or spaces, meaning the filtered result becomes an empty string. The problem explicitly states that an empty string should be considered a palindrome. The input may also contain mixed uppercase and lowercase letters, requiring case normalization. Another tricky case involves numbers, since digits count as alphanumeric characters and must participate in the palindrome comparison.
Approaches
Brute Force Approach
A straightforward way to solve this problem is to first build a cleaned version of the string.
We iterate through the original string and keep only alphanumeric characters, converting letters to lowercase as we go. Once we have this cleaned string, we create its reverse and compare the two strings.
If the cleaned string equals its reverse, then the input is a palindrome.
This approach is correct because the preprocessing exactly matches the problem definition. However, it uses extra memory to construct both the filtered string and its reversed version.
For very large inputs, creating multiple copies of the string is unnecessary and can be avoided.
Key Insight for the Optimal Approach
The key observation is that we do not actually need to construct the cleaned string.
Instead, we can use the two pointers technique directly on the original string:
- One pointer starts at the beginning.
- One pointer starts at the end.
- Skip any non-alphanumeric characters.
- Compare valid characters after converting them to lowercase.
- Move inward after successful comparisons.
This works because a palindrome is fundamentally a symmetry check. We only care whether matching characters at mirrored positions are equal after normalization.
By processing characters in-place, we avoid building extra strings and reduce memory usage.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Build filtered string and compare with reversed version |
| Optimal | O(n) | O(1) | Two pointers process string in-place |
Algorithm Walkthrough
- Initialize two pointers,
leftat the beginning of the string andrightat the end.
These pointers represent the characters we want to compare from opposite ends of the string. 2. Skip non-alphanumeric characters from the left side.
If s[left] is not a letter or digit, increment left. Since these characters should be ignored according to the problem statement, they do not participate in palindrome checking.
3. Skip non-alphanumeric characters from the right side.
Similarly, if s[right] is not alphanumeric, decrement right.
4. Compare the normalized characters.
Once both pointers are positioned at valid characters, convert both to lowercase and compare them.
If they are different, immediately return false, since a palindrome requires mirrored characters to match.
5. Move both pointers inward.
If the characters match, increment left and decrement right to continue checking the remaining portion of the string.
6. Continue until the pointers cross.
If all valid character pairs match, return true.
Why it works
The algorithm maintains the invariant that every pair of valid characters outside the current [left, right] range has already been verified to match. At each step, we compare the next valid mirrored characters after normalization. If any pair differs, the string cannot be a palindrome. If the pointers cross without finding a mismatch, then every mirrored pair matched, proving the string is a valid palindrome.
Python Solution
class Solution:
def isPalindrome(self, s: str) -> bool:
left = 0
right = 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
The implementation follows the algorithm exactly.
We first initialize two pointers, left and right, at opposite ends of the string. The main while loop continues as long as the pointers have not crossed.
Inside the loop, we skip invalid characters using isalnum(), which checks whether a character is alphanumeric. This ensures punctuation, spaces, and symbols are ignored.
After positioning both pointers on valid characters, we normalize the comparison using lower(). If the characters differ, we immediately return False.
If they match, both pointers move inward to continue checking the remaining substring.
If the loop completes without detecting a mismatch, every relevant character pair matched successfully, so we return True.
Go Solution
import "unicode"
func isPalindrome(s string) bool {
left := 0
right := len(s) - 1
for left < right {
for left < right && !unicode.IsLetter(rune(s[left])) && !unicode.IsDigit(rune(s[left])) {
left++
}
for left < right && !unicode.IsLetter(rune(s[right])) && !unicode.IsDigit(rune(s[right])) {
right--
}
leftChar := unicode.ToLower(rune(s[left]))
rightChar := unicode.ToLower(rune(s[right]))
if leftChar != rightChar {
return false
}
left++
right--
}
return true
}
The Go implementation follows the same two pointer strategy as Python.
One implementation difference is character handling. Go strings are byte sequences, so individual characters are accessed using indexing. Since the problem guarantees printable ASCII input, indexing by byte is safe.
Go does not provide an isalnum() equivalent directly on strings, so we use unicode.IsLetter() and unicode.IsDigit() to check validity. We also use unicode.ToLower() for case-insensitive comparison.
Unlike Python, there is no distinction between nil and empty strings here, because the input is guaranteed to be a valid string.
Worked Examples
Example 1
Input:
"A man, a plan, a canal: Panama"
| Step | Left Pointer | Right Pointer | Character Comparison | Action |
|---|---|---|---|---|
| 1 | A |
a |
a == a |
Move inward |
| 2 | space → m |
m |
m == m |
Move inward |
| 3 | a |
a |
a == a |
Move inward |
| 4 | n |
n |
n == n |
Move inward |
| 5 | punctuation skipped | punctuation skipped | Continue | Continue |
| ... | ... | ... | All pairs match | Continue |
| Final | pointers cross | Return true |
The algorithm keeps skipping spaces and punctuation while comparing only normalized alphanumeric characters. Since every mirrored pair matches, the result is true.
Example 2
Input:
"race a car"
| Step | Left Pointer | Right Pointer | Character Comparison | Result |
|---|---|---|---|---|
| 1 | r |
r |
r == r |
Continue |
| 2 | a |
a |
a == a |
Continue |
| 3 | c |
c |
c == c |
Continue |
| 4 | e |
a |
e != a |
Return false |
A mismatch occurs before the pointers meet, proving the string is not a palindrome.
Example 3
Input:
" "
| Step | Left Pointer | Right Pointer | Action |
|---|---|---|---|
| 1 | 0 | 0 | Loop never executes |
| Final | Return true |
After removing non-alphanumeric characters, the string becomes empty, which is considered a palindrome.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each character is visited at most once by either pointer |
| Space | O(1) | Only a few variables are used regardless of input size |
The time complexity is linear because each pointer moves inward without revisiting characters. Even though there are nested loops for skipping invalid characters, every character is skipped at most once.
The space complexity is constant because no additional data structures proportional to the input size are allocated.
Test Cases
solution = Solution()
assert solution.isPalindrome("A man, a plan, a canal: Panama") is True # classic valid palindrome
assert solution.isPalindrome("race a car") is False # mismatch in middle
assert solution.isPalindrome(" ") is True # empty after filtering
assert solution.isPalindrome("") is True # empty string edge case
assert solution.isPalindrome("a") is True # single character
assert solution.isPalindrome("ab") is False # simple non-palindrome
assert solution.isPalindrome("aa") is True # simple palindrome
assert solution.isPalindrome("0P") is False # digit and letter mismatch
assert solution.isPalindrome("Madam") is True # case insensitive match
assert solution.isPalindrome("No 'x' in Nixon") is True # punctuation ignored
assert solution.isPalindrome(".,") is True # only punctuation
assert solution.isPalindrome("12321") is True # numeric palindrome
assert solution.isPalindrome("12345") is False # numeric non-palindrome
assert solution.isPalindrome("Able was I ere I saw Elba") is True # phrase palindrome
assert solution.isPalindrome("a,b,c,c,b,a") is True # punctuation ignored
| Test | Why |
|---|---|
"A man, a plan, a canal: Panama" |
Valid palindrome with spaces and punctuation |
"race a car" |
Standard negative case |
" " |
Empty after filtering |
"" |
Empty string boundary case |
"a" |
Single character input |
"ab" |
Smallest non-palindrome |
"aa" |
Smallest palindrome |
"0P" |
Digit and letter mismatch |
"Madam" |
Case insensitivity |
"No 'x' in Nixon" |
Mixed punctuation and casing |
".," |
Only ignored characters |
"12321" |
Numeric palindrome |
"12345" |
Numeric non-palindrome |
"Able was I ere I saw Elba" |
Long phrase palindrome |
"a,b,c,c,b,a" |
Valid palindrome with separators |
Edge Cases
Strings Containing Only Non-Alphanumeric Characters
An input such as "!@#$" can be tricky because all characters are ignored. A naive implementation might accidentally return false if it expects at least one valid character. In this solution, both pointers skip invalid characters until they cross, and the function correctly returns true.
Mixed Case Characters
Inputs such as "RaceCar" should still count as palindromes despite different letter casing. Without normalization, 'R' and 'r' would not match. The implementation solves this by converting both characters to lowercase before comparison.
Strings Containing Numbers
Digits are part of the palindrome definition and must not be ignored. An input like "12321" should return true, while "12a21" should return false. Because the implementation treats both letters and digits as valid alphanumeric characters, numeric palindromes are handled correctly.