125. Valid Palindrome
Problem Description
You need to determine if a given string is a valid palindrome when considering only alphanumeric characters and ignoring cases.
A palindrome reads the same forward and backward. For this problem, you must:
- Convert all uppercase letters to lowercase
- Remove all non-alphanumeric characters (keep only letters and numbers)
- Check if the resulting string reads the same from left to right as it does from right to left
Given a string s
, return true
if it forms a valid palindrome under these rules, or false
otherwise.
For example:
"A man, a plan, a canal: Panama"
becomes"amanaplanacanalpanama"
after processing, which is a palindrome, so returntrue
"race a car"
becomes"raceacar"
after processing, which is not a palindrome, so returnfalse
The solution uses two pointers starting from opposite ends of the string. The pointers move toward each other, skipping non-alphanumeric characters and comparing the lowercase versions of valid characters. If all valid character pairs match, the string is a palindrome.
Intuition
The key insight is that we don't need to create a new cleaned string to check if it's a palindrome. Instead, we can check the palindrome property while skipping invalid characters on the fly.
Think about how you would manually check if a phrase is a palindrome:
- You'd look at the first and last valid characters
- Compare them (ignoring case)
- Move inward to the next pair of valid characters
- Repeat until you've checked all pairs
This naturally leads to a two-pointer approach. We place one pointer i
at the beginning and another pointer j
at the end of the string.
As we move the pointers toward each other:
- When
i
points to a non-alphanumeric character, we skip it by movingi
forward - When
j
points to a non-alphanumeric character, we skip it by movingj
backward - When both point to valid characters, we compare their lowercase versions
- If they match, we continue by moving both pointers inward
- If they don't match, we know it's not a palindrome
This approach is efficient because:
- We only traverse the string once (O(n) time)
- We don't need extra space to store a cleaned version of the string (O(1) space)
- We can return early as soon as we find a mismatch
The beauty of this solution is that it handles all the requirements (case insensitivity and non-alphanumeric characters) in a single pass without any preprocessing.
Solution Approach
The implementation uses the two-pointer technique to efficiently check if the string is a palindrome without creating a cleaned copy of the string.
Algorithm Steps:
-
Initialize two pointers: Set
i = 0
pointing to the start of the string andj = len(s) - 1
pointing to the end. -
Main loop: Continue while
i < j
:a. Skip non-alphanumeric from left: If
s[i]
is not alphanumeric (checked usings[i].isalnum()
), incrementi
by 1 and continue to the next iteration.b. Skip non-alphanumeric from right: If
s[j]
is not alphanumeric, decrementj
by 1 and continue to the next iteration.c. Compare valid characters: When both
s[i]
ands[j]
are alphanumeric:- Convert both to lowercase using
s[i].lower()
ands[j].lower()
- If they're not equal, return
False
immediately (not a palindrome) - If they match, move both pointers inward:
i = i + 1
andj = j - 1
- Convert both to lowercase using
-
Return result: If the loop completes without finding mismatches, return
True
.
Key Implementation Details:
- The
isalnum()
method checks if a character is alphanumeric (letter or digit) - The
lower()
method handles case-insensitive comparison - The early return on mismatch (
return False
) optimizes performance for non-palindromes - The pointer movement logic ensures we only compare valid characters
Time and Space Complexity:
- Time Complexity:
O(n)
wheren
is the length of the string. Each character is visited at most once. - Space Complexity:
O(1)
as we only use two pointer variables regardless of input size.
This approach elegantly handles all edge cases including empty strings, strings with only non-alphanumeric characters, and strings with mixed cases and punctuation.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the string "A man, a plan, a canal: Panama"
.
Initial Setup:
- String:
"A man, a plan, a canal: Panama"
i = 0
(pointing to 'A')j = 30
(pointing to 'a' at the end)
Step-by-step execution:
Iteration 1:
s[0] = 'A'
is alphanumeric โs[30] = 'a'
is alphanumeric โ- Compare:
'A'.lower() = 'a'
vs'a'.lower() = 'a'
โ Match! - Move pointers:
i = 1
,j = 29
Iteration 2:
s[1] = ' '
is NOT alphanumeric- Skip:
i = 2
, continue
Iteration 3:
s[2] = 'm'
is alphanumeric โs[29] = 'm'
is alphanumeric โ- Compare:
'm'.lower() = 'm'
vs'm'.lower() = 'm'
โ Match! - Move pointers:
i = 3
,j = 28
Iteration 4:
s[3] = 'a'
is alphanumeric โs[28] = 'a'
is alphanumeric โ- Compare:
'a'.lower() = 'a'
vs'a'.lower() = 'a'
โ Match! - Move pointers:
i = 4
,j = 27
Iteration 5:
s[4] = 'n'
is alphanumeric โs[27] = 'n'
is alphanumeric โ- Compare:
'n'.lower() = 'n'
vs'n'.lower() = 'n'
โ Match! - Move pointers:
i = 5
,j = 26
Iteration 6:
s[5] = ','
is NOT alphanumeric- Skip:
i = 6
, continue
Iteration 7:
s[6] = ' '
is NOT alphanumeric- Skip:
i = 7
, continue
Iteration 8:
s[7] = 'a'
is alphanumeric โs[26] = 'a'
is alphanumeric โ- Compare:
'a'.lower() = 'a'
vs'a'.lower() = 'a'
โ Match! - Move pointers:
i = 8
,j = 25
This pattern continues, with the algorithm:
- Skipping spaces, commas, and colons
- Comparing valid alphanumeric characters
- Moving both pointers inward when matches are found
Eventually, the pointers meet in the middle (when i >= j
), and since all comparisons matched, the function returns True
.
Key observations from this walkthrough:
- We never created a cleaned string - we handled everything on the fly
- Non-alphanumeric characters were seamlessly skipped
- Case differences were handled during comparison
- The algorithm efficiently validated the palindrome in a single pass
Solution Implementation
1class Solution:
2 def isPalindrome(self, s: str) -> bool:
3 """
4 Determines if a string is a palindrome, considering only alphanumeric characters
5 and ignoring case differences.
6
7 Args:
8 s: Input string to check
9
10 Returns:
11 True if the string is a palindrome, False otherwise
12 """
13 # Initialize two pointers at the start and end of the string
14 left_index = 0
15 right_index = len(s) - 1
16
17 # Continue checking while the pointers haven't crossed
18 while left_index < right_index:
19 # Skip non-alphanumeric character on the left
20 if not s[left_index].isalnum():
21 left_index += 1
22 # Skip non-alphanumeric character on the right
23 elif not s[right_index].isalnum():
24 right_index -= 1
25 # Check if characters don't match (case-insensitive)
26 elif s[left_index].lower() != s[right_index].lower():
27 return False
28 # Characters match, move both pointers inward
29 else:
30 left_index += 1
31 right_index -= 1
32
33 # All characters matched successfully
34 return True
35
1class Solution {
2 /**
3 * Determines if a string is a palindrome, considering only alphanumeric characters
4 * and ignoring cases.
5 *
6 * @param s the input string to check
7 * @return true if the string is a palindrome, false otherwise
8 */
9 public boolean isPalindrome(String s) {
10 // Initialize two pointers: left pointer starts at beginning, right pointer at end
11 int left = 0;
12 int right = s.length() - 1;
13
14 // Continue comparing characters while pointers haven't crossed
15 while (left < right) {
16 // Get characters at current positions
17 char leftChar = s.charAt(left);
18 char rightChar = s.charAt(right);
19
20 // Skip non-alphanumeric character on the left
21 if (!Character.isLetterOrDigit(leftChar)) {
22 left++;
23 }
24 // Skip non-alphanumeric character on the right
25 else if (!Character.isLetterOrDigit(rightChar)) {
26 right--;
27 }
28 // Compare characters (case-insensitive)
29 else if (Character.toLowerCase(leftChar) != Character.toLowerCase(rightChar)) {
30 // Characters don't match - not a palindrome
31 return false;
32 }
33 else {
34 // Characters match - move both pointers inward
35 left++;
36 right--;
37 }
38 }
39
40 // All alphanumeric characters matched - string is a palindrome
41 return true;
42 }
43}
44
1class Solution {
2public:
3 bool isPalindrome(string s) {
4 // Initialize two pointers: left starts from beginning, right from end
5 int left = 0;
6 int right = s.size() - 1;
7
8 // Continue checking while pointers haven't crossed
9 while (left < right) {
10 // Skip non-alphanumeric character from the left
11 if (!isalnum(s[left])) {
12 ++left;
13 }
14 // Skip non-alphanumeric character from the right
15 else if (!isalnum(s[right])) {
16 --right;
17 }
18 // Compare characters (case-insensitive)
19 // If they don't match, it's not a palindrome
20 else if (tolower(s[left]) != tolower(s[right])) {
21 return false;
22 }
23 // Characters match, move both pointers inward
24 else {
25 ++left;
26 --right;
27 }
28 }
29
30 // All alphanumeric characters matched successfully
31 return true;
32 }
33};
34
1/**
2 * Determines if a string is a valid palindrome, considering only alphanumeric characters
3 * and ignoring cases.
4 * @param s - The input string to check
5 * @returns true if the string is a palindrome, false otherwise
6 */
7function isPalindrome(s: string): boolean {
8 // Initialize two pointers: one at the beginning and one at the end of the string
9 let leftPointer: number = 0;
10 let rightPointer: number = s.length - 1;
11
12 // Regular expression pattern to match alphanumeric characters (letters and digits)
13 const alphanumericPattern: RegExp = /[a-zA-Z0-9]/;
14
15 // Continue comparing characters while the pointers haven't crossed
16 while (leftPointer < rightPointer) {
17 // Skip non-alphanumeric character at the left pointer
18 if (!alphanumericPattern.test(s[leftPointer])) {
19 leftPointer++;
20 }
21 // Skip non-alphanumeric character at the right pointer
22 else if (!alphanumericPattern.test(s[rightPointer])) {
23 rightPointer--;
24 }
25 // Compare the characters at both pointers (case-insensitive)
26 else if (s[leftPointer].toLowerCase() !== s[rightPointer].toLowerCase()) {
27 // Characters don't match, not a palindrome
28 return false;
29 }
30 else {
31 // Characters match, move both pointers inward
32 leftPointer++;
33 rightPointer--;
34 }
35 }
36
37 // All alphanumeric characters matched successfully
38 return true;
39}
40
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. In the worst case, the algorithm needs to traverse the entire string once using the two-pointer approach. Each character is visited at most once by either pointer i
or j
. Even though there are conditional checks for alphanumeric characters, the total number of iterations is still bounded by n
since each iteration either advances i
, decrements j
, or both.
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space for the two pointers i
and j
, regardless of the input size. The operations isalnum()
and lower()
are performed in-place without creating new data structures, and no additional arrays, strings, or other data structures that scale with input size are created.
Common Pitfalls
1. Incorrect Pointer Movement Logic
One of the most common mistakes is moving both pointers simultaneously when encountering non-alphanumeric characters, or forgetting to use continue
statements after moving a single pointer.
Incorrect Implementation:
while left_index < right_index: if not s[left_index].isalnum(): left_index += 1 if not s[right_index].isalnum(): # Bug: This should be 'elif' right_index -= 1 # This comparison might execute even when we just skipped characters! elif s[left_index].lower() != s[right_index].lower(): return False else: left_index += 1 right_index -= 1
The Problem: When the left character is non-alphanumeric, we increment left_index
, but then we still check the right character and potentially compare characters that we haven't properly validated yet.
Solution: Use elif
statements or continue
to ensure only one action happens per iteration:
while left_index < right_index: if not s[left_index].isalnum(): left_index += 1 continue # Skip to next iteration if not s[right_index].isalnum(): right_index -= 1 continue # Skip to next iteration if s[left_index].lower() != s[right_index].lower(): return False left_index += 1 right_index -= 1
2. Infinite Loop When All Characters Are Non-Alphanumeric
Problematic Pattern:
while left_index < right_index: while not s[left_index].isalnum(): # Danger: No bounds check! left_index += 1 while not s[right_index].isalnum(): # Danger: No bounds check! right_index -= 1 # ... comparison logic
The Problem: If the string contains only non-alphanumeric characters (like ".,!@#"
), the inner while loops will go out of bounds causing an IndexError
.
Solution: Always include boundary checks in inner loops:
while left_index < right_index: while left_index < right_index and not s[left_index].isalnum(): left_index += 1 while left_index < right_index and not s[right_index].isalnum(): right_index -= 1 if left_index < right_index: # Check again before comparing if s[left_index].lower() != s[right_index].lower(): return False left_index += 1 right_index -= 1
3. Using <=
Instead of <
in the While Condition
Incorrect:
while left_index <= right_index: # Bug: Should be '<'
The Problem: When left_index == right_index
, we're pointing at the same character in the middle of an odd-length string. Comparing it with itself is unnecessary and could cause issues if the logic isn't structured properly.
Solution: Use <
to stop when pointers meet or cross:
while left_index < right_index:
This correctly handles both even and odd-length strings without unnecessary comparisons.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donโt Miss This!