Files
codetutor/backend/data/questions/valid-palindrome-ii.yaml
2025-05-30 19:18:33 +01:00

204 lines
8.9 KiB
YAML

title: Valid Palindrome II
slug: valid-palindrome-ii
difficulty: easy
leetcode_id: 680
leetcode_url: https://leetcode.com/problems/valid-palindrome-ii/
categories:
- strings
- two-pointers
patterns:
- two-pointers
description: |
Given a string `s`, return `true` *if the* `s` *can be a palindrome after deleting **at most one** character from it*.
constraints: |
- `1 <= s.length <= 10^5`
- `s` consists of lowercase English letters.
examples:
- input: 's = "aba"'
output: "true"
explanation: "The string is already a palindrome, so no deletion is needed."
- input: 's = "abca"'
output: "true"
explanation: "You could delete the character 'c' to get 'aba', which is a palindrome."
- input: 's = "abc"'
output: "false"
explanation: "No matter which character you delete, you cannot form a palindrome."
explanation:
intuition: |
Imagine checking if a word reads the same forwards and backwards by placing two fingers at opposite ends and moving them towards the centre.
For a regular palindrome check, if the characters under your fingers ever mismatch, the string fails immediately. But here we have a **second chance**: we're allowed to remove *one* character and try again.
Think of it like this: when you encounter your first mismatch at positions `left` and `right`, you have two options to "fix" it:
- **Skip the left character**: Check if the substring from `left + 1` to `right` is a palindrome
- **Skip the right character**: Check if the substring from `left` to `right - 1` is a palindrome
If either option produces a valid palindrome, the answer is `true`. This greedy approach works because you only get one deletion, so when characters don't match, you must decide immediately which one to skip.
approach: |
We solve this using a **Two Pointer Approach with One Chance**:
**Step 1: Set up two pointers**
- `left`: Start at index `0` (beginning of string)
- `right`: Start at index `len(s) - 1` (end of string)
&nbsp;
**Step 2: Move pointers inward while characters match**
- While `left < right`, compare `s[left]` and `s[right]`
- If they match, move both pointers inward (`left += 1`, `right -= 1`)
- If they don't match, we've found a problem — proceed to Step 3
&nbsp;
**Step 3: Handle the first mismatch**
- When `s[left] != s[right]`, try both deletion options:
- Check if `s[left+1:right+1]` is a palindrome (skip left character)
- Check if `s[left:right]` is a palindrome (skip right character)
- If either substring is a palindrome, return `true`
- If neither works, return `false`
&nbsp;
**Step 4: Return true if no mismatch found**
- If we complete the loop without finding a mismatch, the string is already a palindrome — return `true`
&nbsp;
This works because we greedily match characters from outside in, and only use our one deletion when absolutely necessary.
common_pitfalls:
- title: Checking All Possible Deletions
description: |
A naive approach is to try deleting each character one by one and check if the result is a palindrome:
```
for i in range(n):
if is_palindrome(s[:i] + s[i+1:]):
return True
```
This results in **O(n^2) time complexity** because you create `n` substrings and check each one in O(n) time. With `s.length <= 10^5`, this approach will be too slow.
Instead, use two pointers to find the *exact* position where a deletion might help, then only check those two possibilities.
wrong_approach: "Try deleting each character one by one"
correct_approach: "Two pointers to find the mismatch, then check two substrings"
- title: Only Trying One Deletion Option
description: |
When you find a mismatch at positions `left` and `right`, you might be tempted to only try skipping one of them. For example, always skipping the left character.
Consider `s = "aguokepatgbnvfqmgmlcupuufxoohdfpgjdmysgvhmvffcnqxjjxqncffvmhvgsymdj"`. When you hit the first mismatch, skipping the wrong character leads to failure even though the string *can* become a palindrome.
Always try **both** options: skip left OR skip right.
wrong_approach: "Only try skipping one character at mismatch"
correct_approach: "Try both skip-left and skip-right, return true if either works"
- title: Creating New Strings for Palindrome Check
description: |
Using string slicing like `s[left+1:right+1]` creates a new string, which uses O(n) space. While this works, you can optimise by passing indices to a helper function that checks palindrome in-place.
For this problem, the O(n) space from slicing is acceptable, but in interviews, mentioning the in-place optimisation shows depth of understanding.
key_takeaways:
- "**Two pointers for palindrome**: The classic technique of comparing from both ends works here with a twist — you get one 'undo' when characters don't match"
- "**Greedy decision point**: When you hit a mismatch, you must try both deletion options since you can't know in advance which will succeed"
- "**Building on fundamentals**: This problem extends the basic palindrome check — many interview problems are variations of simpler ones"
- "**Early termination**: If no mismatch is found, the string is already a palindrome — no deletion needed"
time_complexity: "O(n). We traverse the string at most twice — once with the main two pointers, and potentially once more to verify a substring after a mismatch."
space_complexity: "O(n) with string slicing for the substring palindrome check, or O(1) if using index-based helper function."
solutions:
- approach_name: Two Pointers with Helper Function
is_optimal: true
code: |
def valid_palindrome(s: str) -> bool:
def is_palindrome(left: int, right: int) -> bool:
"""Check if s[left:right+1] is a palindrome using indices."""
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
# Mismatch found — try skipping left or right character
return is_palindrome(left + 1, right) or is_palindrome(left, right - 1)
left += 1
right -= 1
# No mismatch found — already a palindrome
return True
explanation: |
**Time Complexity:** O(n) — We scan through the string at most twice.
**Space Complexity:** O(1) — We only use pointer variables; no extra space proportional to input size.
The helper function checks if a substring is a palindrome using indices, avoiding string slicing. When we find a mismatch, we try both options (skip left or skip right) and return true if either produces a palindrome.
- approach_name: Two Pointers with String Slicing
is_optimal: false
code: |
def valid_palindrome(s: str) -> bool:
def is_palindrome(substring: str) -> bool:
"""Check if a string is a palindrome."""
return substring == substring[::-1]
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
# Try removing left character or right character
skip_left = s[left + 1:right + 1]
skip_right = s[left:right]
return is_palindrome(skip_left) or is_palindrome(skip_right)
left += 1
right -= 1
return True
explanation: |
**Time Complexity:** O(n) — String reversal and comparison are both O(n).
**Space Complexity:** O(n) — String slicing creates new strings.
This version is more readable but uses extra space for the substring copies. The logic is identical: find the first mismatch, then check if either deletion option creates a palindrome.
- approach_name: Brute Force
is_optimal: false
code: |
def valid_palindrome(s: str) -> bool:
def is_palindrome(string: str) -> bool:
return string == string[::-1]
# Check if already a palindrome
if is_palindrome(s):
return True
# Try deleting each character
for i in range(len(s)):
# Create string with character at index i removed
modified = s[:i] + s[i + 1:]
if is_palindrome(modified):
return True
return False
explanation: |
**Time Complexity:** O(n^2) — We try n deletions, each requiring O(n) palindrome check.
**Space Complexity:** O(n) — Creating modified strings.
This brute force approach is correct but too slow for large inputs. It's included to illustrate why the two-pointer optimisation is necessary. With `n = 10^5`, this would perform up to 10 billion operations.