Skip to content

Commit 210d3d5

Browse files
Merge pull request #39 from romankurnovskii/pr-problem-125
Add explanation and solution for LeetCode problem 125 (Valid Palindrome)
2 parents 519e952 + da07035 commit 210d3d5

File tree

2 files changed

+92
-80
lines changed

2 files changed

+92
-80
lines changed

explanations/125/en.md

Lines changed: 64 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
## 125. Valid Palindrome [Easy]
22

3-
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.
3+
https://leetcode.com/problems/valid-palindrome
44

5-
Given a string `s`, return `true` if it is a palindrome, or `false` otherwise.
5+
## Description
6+
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.
7+
8+
Given a string `s`, return `true` *if it is a **palindrome**, or *`false`* otherwise.*
69

710
**Examples**
8-
```text
11+
12+
```tex
913
Example 1:
1014
Input: s = "A man, a plan, a canal: Panama"
1115
Output: true
@@ -20,92 +24,84 @@ Example 3:
2024
Input: s = " "
2125
Output: true
2226
Explanation: s is an empty string "" after removing non-alphanumeric characters.
23-
An empty string is a palindrome.
27+
Since an empty string reads the same forward and backward, it is a palindrome.
2428
```
2529

26-
**Constraints:**
27-
```text
28-
1 <= s.length <= 2 * 10^5
29-
s consists only of printable ASCII characters.
30+
**Constraints**
31+
```tex
32+
- 1 <= s.length <= 2 * 10^5
33+
- s consists only of printable ASCII characters
3034
```
3135

3236
## Explanation
33-
You're given a string `s`. Your task is to determine if it's a palindrome. A palindrome is a sequence that reads the same forwards and backwards. However, there are two special rules for this problem:
34-
1. You should ignore all non-alphanumeric characters (like spaces, commas, colons, etc.).
35-
2. You should treat uppercase and lowercase letters as the same (e.g., 'A' is the same as 'a').
36-
37-
So, "A man, a plan, a canal: Panama" should become "amanaplanacanalpanama" before checking if it's a palindrome.
3837

3938
### Strategy
40-
You are given a string `s`.
41-
The problem asks you to check if `s` is a palindrome after specific filtering and casing rules.
42-
This is a string manipulation and two-pointer problem.
39+
Let's restate the problem: You're given a string that may contain letters, numbers, spaces, and punctuation marks. You need to determine if it's a palindrome after cleaning it up (removing non-alphanumeric characters and converting to lowercase).
4340

44-
**Constraints:**
45-
* `1 <= s.length <= 2 * 10^5`: The string can be quite long, so an O(n) solution is preferred. O(n log n) might also pass, but O(n^2) would be too slow.
46-
* `s consists only of printable ASCII characters`: This simplifies character handling, no complex encodings.
41+
This is a **two-pointer problem** that involves string preprocessing and then checking for palindrome properties.
4742

48-
The most robust and efficient strategy involves processing the string to meet the palindrome rules and then applying a two-pointer approach. You can either pre-process the entire string first, or filter characters on the fly while comparing. The "on-the-fly" method is often slightly more memory-efficient as it doesn't create a new full string immediately, but both are O(N) time and O(N) or O(1) space (depending on how you count auxiliary space for the cleaned string). The "on-the-fly" method often has better constant factors for space.
43+
**What is given?** A string that may contain various characters including letters, numbers, spaces, and punctuation.
4944

50-
Let's focus on the "Two Pointers with On-the-Fly Filtering" strategy, as it's generally preferred for interviews due to its space efficiency and direct manipulation of the original string.
45+
**What is being asked?** Determine if the cleaned string (only alphanumeric characters, all lowercase) is a palindrome.
5146

52-
**Decomposition:**
53-
1. Initialize two pointers, `left` at the beginning of the string and `right` at the end.
54-
2. While `left` is less than `right`:
55-
a. Move `left` inwards until it points to an alphanumeric character.
56-
b. Move `right` inwards until it points to an alphanumeric character.
57-
c. If `left` is still less than `right` (meaning both found valid characters):
58-
i. Compare the characters at `left` and `right`, ignoring case.
59-
ii. If they don't match, return `false` (not a palindrome).
60-
iii. If they match, move `left` one step right and `right` one step left.
61-
d. If `left` becomes `right` or `left` crosses `right`, the loop ends.
62-
3. If the loop completes, it means all compared alphanumeric characters matched, so return `true` (it's a palindrome).
47+
**Constraints:** The string can be up to 200,000 characters long and contains only printable ASCII characters.
6348

64-
### Steps
65-
Let's use the example `s = "A man, a plan, a canal: Panama"`
49+
**Edge cases:**
50+
- Empty string (should return true)
51+
- String with only non-alphanumeric characters (should return true)
52+
- Single character (should return true)
53+
- String with mixed case and punctuation
54+
55+
**High-level approach:**
56+
The solution involves two main steps:
57+
1. **Preprocessing**: Clean the string by removing non-alphanumeric characters and converting to lowercase
58+
2. **Palindrome check**: Use two pointers to check if the cleaned string reads the same forward and backward
6659

67-
1. Initialize `left = 0`, `right = len(s) - 1` (which is `29`).
68-
`s[left]` is 'A', `s[right]` is 'a'.
60+
**Decomposition:**
61+
1. **Clean the string**: Remove all non-alphanumeric characters and convert to lowercase
62+
2. **Initialize pointers**: Place one pointer at the start and one at the end
63+
3. **Compare characters**: Move pointers inward while comparing characters
64+
4. **Return result**: Return true if all characters match, false otherwise
6965

70-
2. **Loop starts (`left < right` is `0 < 29` which is True):**
66+
**Brute force vs. optimized strategy:**
67+
- **Brute force**: Create a new cleaned string and then check if it equals its reverse. This takes O(n) time and O(n) space.
68+
- **Optimized**: Use two pointers to check palindrome property in-place. This takes O(n) time and O(1) space.
7169

72-
* **Find alphanumeric `s[left]`:**
73-
* `s[0]` is 'A'. `s[0].isalnum()` is True. `left` stays `0`.
74-
* **Find alphanumeric `s[right]`:**
75-
* `s[29]` is 'a'. `s[29].isalnum()` is True. `right` stays `29`.
76-
* **Compare:**
77-
* `s[0].lower()` ('a') vs `s[29].lower()` ('a'). They match.
78-
* **Move pointers:** `left` becomes `1`, `right` becomes `28`.
70+
### Steps
71+
Let's walk through the solution step by step using the first example: `s = "A man, a plan, a canal: Panama"`
7972

80-
3. **Loop continues (`left < right` is `1 < 28` which is True):**
73+
**Step 1: Preprocessing**
74+
- Remove all non-alphanumeric characters: spaces, commas, colons
75+
- Convert all letters to lowercase
76+
- Result: `"amanaplanacanalpanama"`
8177

82-
* `s[1]` is ' '. `s[1].isalnum()` is False. `left` increments to `2`.
83-
* `s[2]` is 'm'. `s[2].isalnum()` is True. `left` stays `2`.
84-
* `s[28]` is 'm'. `s[28].isalnum()` is True. `right` stays `28`.
85-
* **Compare:**
86-
* `s[2].lower()` ('m') vs `s[28].lower()` ('m'). They match.
87-
* **Move pointers:** `left` becomes `3`, `right` becomes `27`.
78+
**Step 2: Initialize pointers**
79+
- `left = 0` (points to the first character: 'a')
80+
- `right = 24` (points to the last character: 'a')
8881

89-
4. **Loop continues (`left < right` is `3 < 27` which is True):**
82+
**Step 3: Compare characters**
83+
- `s[left] = 'a'`, `s[right] = 'a'`
84+
- `'a' == 'a'` ✓, so move both pointers inward
85+
- `left = 1`, `right = 23`
9086

91-
* `s[3]` is 'a'. `left` stays `3`.
92-
* `s[27]` is 'a'. `right` stays `27`.
93-
* **Compare:** `s[3].lower()` ('a') vs `s[27].lower()` ('a'). Match.
94-
* **Move pointers:** `left` becomes `4`, `right` becomes `26`.
87+
**Step 4: Continue comparison**
88+
- `s[left] = 'm'`, `s[right] = 'm'`
89+
- `'m' == 'm'` ✓, so move both pointers inward
90+
- `left = 2`, `right = 22`
9591

96-
...This process repeats. Non-alphanumeric characters (spaces, commas, colons) will cause one of the inner `while` loops to advance a pointer. For example, if `s[left]` is a space, `left` will increment until it finds an alphanumeric character. The same happens for `right` moving inwards.
92+
**Step 5: Continue until pointers meet**
93+
- Continue this process, comparing characters at both ends
94+
- Move pointers inward after each successful comparison
95+
- Stop when `left >= right`
9796

98-
Eventually, if `s = "A man, a plan, a canal: Panama"`, all corresponding alphanumeric characters will match. The pointers will cross or meet (`left >= right`). When `left` is no longer less than `right`, the `while left < right` loop terminates.
97+
**Step 6: Check result**
98+
- If we reach the middle without finding a mismatch, it's a palindrome
99+
- In this case, all characters match, so return `true`
99100

100-
At this point, since no mismatches were found, the function returns `true`.
101+
**Why this works:**
102+
A palindrome reads the same forward and backward. By using two pointers that start at opposite ends and move inward, we can efficiently check this property. If at any point the characters don't match, we know it's not a palindrome. If we reach the middle with all characters matching, it must be a palindrome.
101103

102-
Example: `s = "race a car"`
103-
1. Initialize `left = 0`, `right = 9`.
104-
`s[0]` is 'r', `s[9]` is 'r'.
105-
2. Loop:
106-
* `left=0` ('r'), `right=9` ('r'). Match. `left=1`, `right=8`.
107-
* `left=1` ('a'), `right=8` ('a'). Match. `left=2`, `right=7`.
108-
* `left=2` ('c'), `right=7` ('c'). Match. `left=3`, `right=6`.
109-
* `left=3` ('e'), `right=6` ('a'). **Mismatch!** ('e' != 'a'). Return `false`.
104+
> **Note:** The key insight is that we don't need to create a new cleaned string. We can process the original string character by character, skipping non-alphanumeric characters and converting case on-the-fly.
110105
111-
This approach is efficient because it processes each character at most a constant number of times (once by `left`, once by `right`). So, time complexity is O(n). Space complexity is O(1) because you're only using a couple of pointers and not creating new large data structures.
106+
**Time Complexity:** O(n) - we process each character at most once
107+
**Space Complexity:** O(1) - we only use a constant amount of extra space for the pointers

solutions/125/01.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
1-
def is_palindrome(s: str) -> bool:
2-
left = 0 # Pointer starting from the beginning of the string
3-
right = len(s) - 1 # Pointer starting from the end of the string
4-
1+
def isPalindrome(s):
2+
"""
3+
Check if a string is a palindrome after removing non-alphanumeric characters
4+
and converting to lowercase.
5+
6+
Args:
7+
s: str - Input string that may contain various characters
8+
9+
Returns:
10+
bool - True if the cleaned string is a palindrome, False otherwise
11+
"""
12+
# Initialize two pointers
13+
left = 0
14+
right = len(s) - 1
15+
16+
# Use two pointers to check palindrome property
517
while left < right:
18+
# Skip non-alphanumeric characters from left
619
while left < right and not s[left].isalnum():
720
left += 1
8-
21+
22+
# Skip non-alphanumeric characters from right
923
while left < right and not s[right].isalnum():
1024
right -= 1
11-
25+
26+
# If pointers haven't crossed, compare characters
1227
if left < right:
28+
# Convert to lowercase and compare
1329
if s[left].lower() != s[right].lower():
14-
res = False
15-
return res
16-
30+
return False
31+
32+
# Move pointers inward
1733
left += 1
1834
right -= 1
19-
20-
res = True
21-
return res
35+
36+
# If we reach here, all characters matched
37+
return True

0 commit comments

Comments
 (0)