204 lines
8.9 KiB
YAML
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)
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
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.
|