188 lines
8.0 KiB
YAML
188 lines
8.0 KiB
YAML
title: Valid Palindrome
|
|
slug: valid-palindrome
|
|
difficulty: easy
|
|
leetcode_id: 125
|
|
leetcode_url: https://leetcode.com/problems/valid-palindrome/
|
|
categories:
|
|
- strings
|
|
- two-pointers
|
|
patterns:
|
|
- two-pointers
|
|
|
|
function_signature: "def is_palindrome(s: str) -> bool:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { s: "A man, a plan, a canal: Panama" }
|
|
expected: true
|
|
- input: { s: "race a car" }
|
|
expected: false
|
|
- input: { s: " " }
|
|
expected: true
|
|
hidden:
|
|
- input: { s: "a" }
|
|
expected: true
|
|
- input: { s: "ab" }
|
|
expected: false
|
|
- input: { s: "Aa" }
|
|
expected: true
|
|
- input: { s: "" }
|
|
expected: true
|
|
- input: { s: ".,!" }
|
|
expected: true
|
|
- input: { s: "0P" }
|
|
expected: false
|
|
- input: { s: "Was it a car or a cat I saw?" }
|
|
expected: true
|
|
|
|
description: |
|
|
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.
|
|
|
|
Given a string `s`, return `true` *if it is a **palindrome***, or `false` *otherwise*.
|
|
|
|
constraints: |
|
|
- `1 <= s.length <= 2 * 10^5`
|
|
- `s` consists only of printable ASCII characters.
|
|
|
|
examples:
|
|
- input: 's = "A man, a plan, a canal: Panama"'
|
|
output: "true"
|
|
explanation: '"amanaplanacanalpanama" is a palindrome.'
|
|
- input: 's = "race a car"'
|
|
output: "false"
|
|
explanation: '"raceacar" is not a palindrome.'
|
|
- input: 's = " "'
|
|
output: "true"
|
|
explanation: 's is an empty string "" after removing non-alphanumeric characters. Since an empty string reads the same forward and backward, it is a palindrome.'
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you have a word written on a piece of paper, and you want to check if it reads the same when you flip the paper upside down (ignoring case and punctuation).
|
|
|
|
The key insight is that a palindrome has **mirror symmetry** — the first character matches the last, the second matches the second-to-last, and so on. You only need to compare characters **from opposite ends** moving toward the middle.
|
|
|
|
Think of it like this: place one finger at the start of the string and another at the end. Move them toward each other, comparing alphanumeric characters as you go. If any pair doesn't match (after normalising case), it's not a palindrome. If they meet in the middle without a mismatch, it is.
|
|
|
|
The clever part is **skipping non-alphanumeric characters** on the fly. Instead of building a cleaned string first, we simply advance our pointers past any characters that aren't letters or digits.
|
|
|
|
approach: |
|
|
We solve this using the **Two Pointers** technique:
|
|
|
|
**Step 1: Initialise two pointers**
|
|
|
|
- `left`: Starts at index `0` (beginning of the string)
|
|
- `right`: Starts at index `len(s) - 1` (end of the string)
|
|
|
|
|
|
|
|
**Step 2: Move pointers toward each other**
|
|
|
|
- While `left < right`:
|
|
- Skip non-alphanumeric characters by advancing `left` while `s[left]` is not alphanumeric
|
|
- Skip non-alphanumeric characters by decrementing `right` while `s[right]` is not alphanumeric
|
|
- Compare `s[left].lower()` with `s[right].lower()`
|
|
- If they don't match, return `false` immediately
|
|
- If they match, move `left` forward and `right` backward
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- If the loop completes without finding a mismatch, return `true`
|
|
|
|
|
|
|
|
This approach processes the string in-place without creating a cleaned copy, achieving O(1) space complexity while maintaining O(n) time.
|
|
|
|
common_pitfalls:
|
|
- title: Creating a Cleaned String First
|
|
description: |
|
|
A common approach is to first filter the string to keep only alphanumeric characters, convert to lowercase, then check if it equals its reverse:
|
|
|
|
```python
|
|
cleaned = ''.join(c.lower() for c in s if c.isalnum())
|
|
return cleaned == cleaned[::-1]
|
|
```
|
|
|
|
While this works and is easy to understand, it uses **O(n) extra space** for the cleaned string. The two-pointer approach achieves the same result with O(1) space by processing in-place.
|
|
wrong_approach: "Filter and reverse comparison"
|
|
correct_approach: "Two pointers comparing in-place"
|
|
|
|
- title: Forgetting to Skip Non-Alphanumeric Characters
|
|
description: |
|
|
If you simply compare `s[left]` and `s[right]` without skipping punctuation and spaces, you'll get wrong answers.
|
|
|
|
For example, `"A man, a plan, a canal: Panama"` would fail because `'A'` would be compared with `'a'` (correct), but then `' '` would be compared with `'m'` (incorrect).
|
|
|
|
Always advance the pointers past non-alphanumeric characters before comparing.
|
|
|
|
- title: Case Sensitivity
|
|
description: |
|
|
Forgetting to convert characters to the same case before comparison will cause failures.
|
|
|
|
`'A'` and `'a'` should be considered equal, but in ASCII they have different values (65 vs 97). Always use `.lower()` or `.upper()` when comparing.
|
|
|
|
- title: Boundary Conditions in Pointer Movement
|
|
description: |
|
|
When skipping non-alphanumeric characters, ensure you don't move the pointer out of bounds. Always check `left < right` during the skip loops, not just in the main loop.
|
|
|
|
For a string like `".,,"`, both pointers need to skip all characters without causing an index error.
|
|
|
|
key_takeaways:
|
|
- "**Two Pointers pattern**: Comparing elements from opposite ends is a fundamental technique for palindrome problems and many array/string problems"
|
|
- "**In-place processing**: Skipping unwanted characters during traversal is more space-efficient than building a filtered copy"
|
|
- "**Normalisation**: When comparing text, always consider case sensitivity and which characters should be included"
|
|
- "**Foundation for variants**: This technique extends to problems like Valid Palindrome II (remove at most one character) and checking palindromes in linked lists"
|
|
|
|
time_complexity: "O(n). Each character is visited at most once by either the left or right pointer."
|
|
space_complexity: "O(1). We only use two integer pointers regardless of input size."
|
|
|
|
solutions:
|
|
- approach_name: Two Pointers
|
|
is_optimal: true
|
|
code: |
|
|
def is_palindrome(s: str) -> bool:
|
|
left = 0
|
|
right = len(s) - 1
|
|
|
|
while left < right:
|
|
# Skip non-alphanumeric characters from the left
|
|
while left < right and not s[left].isalnum():
|
|
left += 1
|
|
|
|
# Skip non-alphanumeric characters from the right
|
|
while left < right and not s[right].isalnum():
|
|
right -= 1
|
|
|
|
# Compare characters (case-insensitive)
|
|
if s[left].lower() != s[right].lower():
|
|
return False
|
|
|
|
# Move pointers toward the center
|
|
left += 1
|
|
right -= 1
|
|
|
|
return True
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each character is visited at most once.
|
|
|
|
**Space Complexity:** O(1) — Only two integer pointers used.
|
|
|
|
We use two pointers starting from opposite ends. For each iteration, we skip any non-alphanumeric characters, then compare the alphanumeric characters (case-insensitively). If all pairs match, the string is a palindrome.
|
|
|
|
- approach_name: Filter and Reverse
|
|
is_optimal: false
|
|
code: |
|
|
def is_palindrome(s: str) -> bool:
|
|
# Build a cleaned string with only lowercase alphanumeric chars
|
|
cleaned = ''.join(char.lower() for char in s if char.isalnum())
|
|
|
|
# Compare with its reverse
|
|
return cleaned == cleaned[::-1]
|
|
explanation: |
|
|
**Time Complexity:** O(n) — One pass to filter, one pass to reverse, one pass to compare.
|
|
|
|
**Space Complexity:** O(n) — The cleaned string and its reverse each take O(n) space.
|
|
|
|
This approach is more intuitive but less space-efficient. We first create a cleaned version of the string containing only lowercase alphanumeric characters, then check if it equals its reverse. While correct, the two-pointer approach is preferred for better space efficiency.
|