Valid Palindrome II
Description
Given a string s, determine whether you can make it a palindrome by removing at most one character from it.
A palindrome is a word that reads the same forwards and backwards. For example, "aba" and "racecar" are palindromes, while "abc" is not.
Your task is to return true if the string is already a palindrome, or if it becomes one after deleting exactly one character. Return false if even removing one character cannot produce a palindrome.
Examples
Example 1
Input: s = "aba"
Output: true
Explanation: The string "aba" is already a palindrome — it reads the same from left to right as from right to left. No deletion is needed, so the answer is true.
Example 2
Input: s = "abca"
Output: true
Explanation: The string "abca" is not a palindrome as-is. However, if we delete the character 'c' at index 2, we get "aba", which is a palindrome. Since we only needed one deletion, the answer is true.
Example 3
Input: s = "abc"
Output: false
Explanation: No single deletion can turn "abc" into a palindrome. Removing 'a' gives "bc" (not a palindrome), removing 'b' gives "ac" (not a palindrome), and removing 'c' gives "ab" (not a palindrome). The answer is false.
Constraints
- 1 ≤ s.length ≤ 10^5
- s consists of lowercase English letters
Editorial
Brute Force
Intuition
The most direct way to solve this is to try every possible deletion. We know the string can have at most one character removed. So, for each position in the string, we can remove the character at that position, build the resulting string, and check if it is a palindrome.
If the original string is already a palindrome, we return true immediately. Otherwise, we try removing the character at index 0, then index 1, then index 2, and so on — checking each time whether the shortened string reads the same forwards and backwards.
Think of it like editing a word on paper: you cover up one letter at a time with your finger and read what remains. If any single cover-up produces a palindrome, the answer is yes.
Step-by-Step Explanation
Let's trace through with s = "abca":
Step 1: First, check if "abca" is already a palindrome.
- Compare s[0]='a' with s[3]='a' → match
- Compare s[1]='b' with s[2]='c' → mismatch
- "abca" is NOT a palindrome. We must try deletions.
Step 2: Try removing index 0. Remaining string = "bca".
- Compare 'b' with 'a' → mismatch. Not a palindrome.
Step 3: Try removing index 1. Remaining string = "aca".
- Compare 'a' with 'a' → match
- Middle character 'c' is fine (single center).
- "aca" IS a palindrome!
Step 4: Since we found a single deletion that produces a palindrome, return true.
(In the worst case we would try all n deletions before concluding.)
Brute Force — Try Every Single Deletion — Watch how we remove one character at a time and check if the remaining string is a palindrome, iterating through all positions until a valid deletion is found.
Algorithm
- Check if the string
sis already a palindrome. If yes, returntrue. - For each index
ifrom 0 to n-1:
a. Build a new string by removing the character at indexi
b. Check if this new string is a palindrome
c. If it is, returntrue - If no single deletion produces a palindrome, return
false
Code
class Solution {
public:
bool isPalindrome(const string& s) {
int left = 0, right = s.size() - 1;
while (left < right) {
if (s[left] != s[right]) return false;
left++;
right--;
}
return true;
}
bool validPalindrome(string s) {
if (isPalindrome(s)) return true;
for (int i = 0; i < s.size(); i++) {
string modified = s.substr(0, i) + s.substr(i + 1);
if (isPalindrome(modified)) return true;
}
return false;
}
};class Solution:
def validPalindrome(self, s: str) -> bool:
def is_palindrome(text: str) -> bool:
return text == text[::-1]
if is_palindrome(s):
return True
for i in range(len(s)):
modified = s[:i] + s[i + 1:]
if is_palindrome(modified):
return True
return Falseclass Solution {
private boolean isPalindrome(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) return false;
left++;
right--;
}
return true;
}
public boolean validPalindrome(String s) {
if (isPalindrome(s)) return true;
for (int i = 0; i < s.length(); i++) {
String modified = s.substring(0, i) + s.substring(i + 1);
if (isPalindrome(modified)) return true;
}
return false;
}
}Complexity Analysis
Time Complexity: O(n²)
We try up to n deletions. For each deletion, we build a new string of length n-1 in O(n) time and check if it is a palindrome in O(n) time. So the total work is n × O(n) = O(n²).
Space Complexity: O(n)
Each time we build the modified string, it takes O(n) space. Only one modified string exists at a time, so the extra space is O(n).
Why This Approach Is Not Efficient
The brute force approach runs in O(n²) time. With n up to 10^5, that means up to 10^10 operations — far too slow for typical time limits of 1-2 seconds.
The core waste is that we treat every position as equally likely to be the character that needs deletion. But think about it: if the string is "almost" a palindrome, the problematic character must be near the point where the palindrome property first breaks. We do not need to try deleting characters at positions far from the mismatch.
The key insight: if we use two pointers converging from both ends and they first disagree at positions left and right, then the only characters worth deleting are s[left] or s[right]. No other deletion can fix the mismatch. This reduces the problem from trying n deletions to trying at most 2.
Optimal Approach - Two Pointers with Greedy Skip
Intuition
Start two pointers at the beginning and end of the string, moving them toward each other. As long as the characters at both pointers match, we shrink the window inward — these matching outer characters are already palindromic and need no deletion.
The interesting moment comes when we hit the first mismatch at positions left and right. At this point, we know that at least one of these two characters must be removed for the string to possibly be a palindrome. We have exactly two choices:
- Skip the left character: Check if the substring from
left+1torightis a palindrome. - Skip the right character: Check if the substring from
lefttoright-1is a palindrome.
If either option gives a palindrome, the answer is true. If neither works, no single deletion can save the string.
Think of it like two people reading the same book — one from the front, one from the back. They compare pages as they go. When they first disagree, one of them skips their page and they check if the rest still matches. If skipping one person's page works, the book is "almost" the same from both directions.
Step-by-Step Explanation
Let's trace through with s = "abca":
Step 1: Initialize two pointers: left = 0, right = 3.
Step 2: Compare s[0] = 'a' with s[3] = 'a'. They match! Move pointers inward: left = 1, right = 2.
Step 3: Compare s[1] = 'b' with s[2] = 'c'. Mismatch! We have found the first disagreement. We now have two choices to try.
Step 4: Choice 1 — skip the left character. Check if s[2..2] = "c" is a palindrome. A single character is always a palindrome. ✓
Step 5: Since Choice 1 succeeds, we do not even need to try Choice 2. The substring from left+1 to right is a palindrome, which means deleting s[left] = 'b' makes the whole string a palindrome.
Step 6: Return true.
Let's also trace a failing case, s = "abc":
Step 7: left = 0, right = 2. Compare s[0] = 'a' with s[2] = 'c'. Mismatch on the very first comparison.
Step 8: Choice 1 — skip left: check s[1..2] = "bc". Is 'b' == 'c'? No. Not a palindrome.
Step 9: Choice 2 — skip right: check s[0..1] = "ab". Is 'a' == 'b'? No. Not a palindrome.
Step 10: Neither choice works. Return false.
Two Pointers with Greedy Skip — Valid Palindrome II — Watch how two pointers converge from both ends. On the first mismatch, we try skipping one character from either side and check if the remaining substring is a palindrome.
Algorithm
- Define a helper function
isPalindrome(s, left, right)that checks whether the substrings[left..right]is a palindrome using two pointers. - Initialize two pointers:
left = 0,right = s.length - 1. - While
left < right:
a. Ifs[left] == s[right], move both pointers inward (left++,right--).
b. Ifs[left] != s[right], we have a mismatch. Try two options:- Check if
s[left+1..right]is a palindrome (skip left character) - Check if
s[left..right-1]is a palindrome (skip right character) - Return
trueif either option is a palindrome; otherwise returnfalse.
- Check if
- If the loop completes without mismatch, the string is already a palindrome — return
true.
Code
class Solution {
public:
bool checkPalindrome(const string& s, int left, int right) {
while (left < right) {
if (s[left] != s[right]) return false;
left++;
right--;
}
return true;
}
bool validPalindrome(string s) {
int left = 0, right = s.size() - 1;
while (left < right) {
if (s[left] != s[right]) {
// Try skipping left or skipping right
return checkPalindrome(s, left + 1, right) ||
checkPalindrome(s, left, right - 1);
}
left++;
right--;
}
return true;
}
};class Solution:
def validPalindrome(self, s: str) -> bool:
def check_palindrome(left: int, right: int) -> bool:
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]:
# Try skipping left character or right character
return check_palindrome(left + 1, right) or check_palindrome(left, right - 1)
left += 1
right -= 1
return Trueclass Solution {
private boolean checkPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) return false;
left++;
right--;
}
return true;
}
public boolean validPalindrome(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
// Try skipping left or skipping right
return checkPalindrome(s, left + 1, right) ||
checkPalindrome(s, left, right - 1);
}
left++;
right--;
}
return true;
}
}Complexity Analysis
Time Complexity: O(n)
The main two-pointer pass traverses the string from both ends toward the center, performing at most n/2 comparisons. When a mismatch is found, the helper function checkPalindrome checks a substring that is at most n-2 characters long, also in O(n). So the total work is O(n) + O(n) = O(n). We never revisit characters or perform nested iterations.
Space Complexity: O(1)
We only use a constant number of integer variables (left, right pointers). No additional strings or data structures are created — the helper function works on the original string using index bounds. This is a significant improvement over the brute force, which required O(n) space for building modified strings.