680. Valid Palindrome II
Problem Description
You are given a string s
. Your task is to determine if this string can become a palindrome by deleting at most one character from it.
A palindrome is a string that reads the same forwards and backwards. For example, "racecar" and "noon" are palindromes.
The key constraint is that you can delete either:
- Zero characters (the string is already a palindrome), or
- Exactly one character from any position in the string
You need to return true
if the string can be made into a palindrome under these conditions, and false
otherwise.
Examples:
- If
s = "aba"
, returntrue
(already a palindrome, no deletion needed) - If
s = "abca"
, returntrue
(delete 'c' to get "aba" which is a palindrome) - If
s = "abc"
, returnfalse
(no single deletion can make it a palindrome)
The solution uses a two-pointer technique. Starting from both ends of the string, we compare characters. When we find a mismatch, we have two options: skip the left character or skip the right character. We check if either option results in a palindrome for the remaining substring. If we never find a mismatch, the string is already a palindrome.
Intuition
The natural way to check if a string is a palindrome is to compare characters from both ends moving towards the center. If all corresponding pairs match, it's a palindrome.
Now, what happens when we're allowed to delete at most one character? Let's think through this step by step.
When we're comparing characters from both ends and find a mismatch, we have a decision to make. For example, if we have s = "abca"
and we're comparing s[0] = 'a'
with s[3] = 'a'
- they match. Next, we compare s[1] = 'b'
with s[2] = 'c'
- they don't match.
At this mismatch point, we know one of these two characters needs to be "deleted" (or skipped) for the string to potentially become a palindrome. But which one? We don't know immediately, so we need to try both possibilities:
- Skip the left character (
'b'
) and check if the remaining part is a palindrome - Skip the right character (
'c'
) and check if the remaining part is a palindrome
If either option works, then we can make the string a palindrome with one deletion.
The key insight is that we only get one chance to handle a mismatch. Once we encounter the first mismatch and decide to skip a character, the remaining substring must be a perfect palindrome - no more deletions allowed.
This leads us to a two-pointer approach where:
- We start with pointers at both ends
- Move them towards each other as long as characters match
- When we hit a mismatch, we branch into two scenarios (skip left or skip right)
- For each scenario, we check if the remaining part forms a valid palindrome
- If we never hit a mismatch, the string is already a palindrome
Learn more about Greedy and Two Pointers patterns.
Solution Approach
The implementation uses the two-pointer technique to efficiently solve this problem.
Main Algorithm Structure:
-
Helper Function
check(i, j)
:- This function verifies if the substring from index
i
toj
is a palindrome - It uses two pointers moving towards each other
- Returns
False
immediately if any mismatch is found - Returns
True
if all characters match
- This function verifies if the substring from index
-
Main Logic:
- Initialize two pointers:
i = 0
(left) andj = len(s) - 1
(right) - Move pointers towards each other comparing characters at each step
- Initialize two pointers:
Step-by-step Implementation:
i, j = 0, len(s) - 1
while i < j:
if s[i] != s[j]:
# Found first mismatch - try both options
return check(i, j - 1) or check(i + 1, j)
i, j = i + 1, j - 1
return True
Key Decision Point:
When s[i] != s[j]
, we have two choices:
check(i, j - 1)
: Skip the character at positionj
(right pointer)check(i + 1, j)
: Skip the character at positioni
(left pointer)
We use the logical or
operator because we only need one of these options to work.
Example Walkthrough with s = "abca"
:
- Compare
s[0]='a'
withs[3]='a'
โ match, move pointers - Compare
s[1]='b'
withs[2]='c'
โ mismatch - Try
check(1, 1)
(skip right 'c'): checks if "b" is palindrome โTrue
- Return
True
Time Complexity: O(n)
where n
is the length of the string. In the worst case, we traverse the string once in the main loop and once more in the helper function.
Space Complexity: O(1)
as we only use a constant amount of extra space for pointers.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "raceacar"
.
Initial Setup:
- String:
"raceacar"
(indices 0-7) - Left pointer
i = 0
, Right pointerj = 7
Step-by-step Execution:
-
First comparison:
s[0]='r'
vss[7]='r'
- They match! Move pointers inward
- Update:
i = 1
,j = 6
-
Second comparison:
s[1]='a'
vss[6]='a'
- They match! Move pointers inward
- Update:
i = 2
,j = 5
-
Third comparison:
s[2]='c'
vss[5]='c'
- They match! Move pointers inward
- Update:
i = 3
,j = 4
-
Fourth comparison:
s[3]='e'
vss[4]='a'
- Mismatch found! Now we try both deletion options:
Option 1: Skip left character (delete 'e' at index 3)
- Call
check(4, 4)
to verify if substring from index 4 to 4 is palindrome - This checks just "a", which is trivially a palindrome
- Returns
True
Option 2: Skip right character (delete 'a' at index 4)
- Call
check(3, 3)
to verify if substring from index 3 to 3 is palindrome - This checks just "e", which is trivially a palindrome
- Returns
True
-
Final Result: Since
check(4, 4)
returnsTrue
, the function returnsTrue
The string "raceacar" can become a palindrome "racecar" by deleting the 'a' at index 4.
Visual representation of the process:
r a c e a c a r ^ ^ (match: r=r) ^ ^ (match: a=a) ^ ^ (match: c=c) ^ ^ (mismatch: eโ a) โ Try deleting one: - Delete 'e': rac_acar โ "racacar" (check remaining) - Delete 'a': race_car โ "racecar" โ (palindrome!)
Solution Implementation
1class Solution:
2 def validPalindrome(self, s: str) -> bool:
3 """
4 Check if a string can be a palindrome after deleting at most one character.
5
6 Args:
7 s: Input string to check
8
9 Returns:
10 True if string can be palindrome with at most one deletion, False otherwise
11 """
12
13 def is_palindrome(left: int, right: int) -> bool:
14 """
15 Helper function to check if substring s[left:right+1] is a palindrome.
16
17 Args:
18 left: Starting index of substring
19 right: Ending index of substring
20
21 Returns:
22 True if substring is palindrome, False otherwise
23 """
24 while left < right:
25 if s[left] != s[right]:
26 return False
27 left += 1
28 right -= 1
29 return True
30
31 # Initialize two pointers at the beginning and end of string
32 left = 0
33 right = len(s) - 1
34
35 # Check characters from both ends moving towards center
36 while left < right:
37 if s[left] != s[right]:
38 # If mismatch found, try deleting either left or right character
39 # and check if remaining substring is palindrome
40 return is_palindrome(left, right - 1) or is_palindrome(left + 1, right)
41
42 # Move pointers towards center
43 left += 1
44 right -= 1
45
46 # String is already a palindrome without any deletion
47 return True
48
1class Solution {
2 private char[] charArray;
3
4 /**
5 * Determines if a string can be a palindrome after deleting at most one character.
6 * Uses two-pointer technique to check from both ends of the string.
7 *
8 * @param s The input string to validate
9 * @return true if the string is a palindrome or can become one by removing at most one character
10 */
11 public boolean validPalindrome(String s) {
12 // Convert string to character array for efficient access
13 this.charArray = s.toCharArray();
14
15 // Initialize two pointers: left pointer at start, right pointer at end
16 int left = 0;
17 int right = charArray.length - 1;
18
19 // Move pointers toward each other
20 while (left < right) {
21 // If characters at current positions don't match
22 if (charArray[left] != charArray[right]) {
23 // Try two possibilities:
24 // 1. Skip the character at left pointer and check remaining substring
25 // 2. Skip the character at right pointer and check remaining substring
26 return isPalindrome(left + 1, right) || isPalindrome(left, right - 1);
27 }
28 // Move both pointers inward
29 left++;
30 right--;
31 }
32
33 // If we've checked all characters without mismatch, it's already a palindrome
34 return true;
35 }
36
37 /**
38 * Helper method to check if a substring is a palindrome.
39 *
40 * @param left Starting index of the substring
41 * @param right Ending index of the substring
42 * @return true if the substring from left to right is a palindrome
43 */
44 private boolean isPalindrome(int left, int right) {
45 // Check characters from both ends moving toward the center
46 while (left < right) {
47 // If any pair of characters doesn't match, it's not a palindrome
48 if (charArray[left] != charArray[right]) {
49 return false;
50 }
51 // Move both pointers inward
52 left++;
53 right--;
54 }
55 // All character pairs matched, substring is a palindrome
56 return true;
57 }
58}
59
1class Solution {
2public:
3 bool validPalindrome(string s) {
4 // Lambda function to check if substring from left to right is a palindrome
5 auto isPalindrome = [&](int left, int right) {
6 // Use two pointers to check characters from both ends
7 while (left < right) {
8 if (s[left] != s[right]) {
9 return false;
10 }
11 left++;
12 right--;
13 }
14 return true;
15 };
16
17 // Initialize two pointers at the beginning and end of string
18 int left = 0;
19 int right = s.size() - 1;
20
21 // Check characters from both ends moving towards center
22 while (left < right) {
23 // If characters don't match, we have one chance to delete
24 if (s[left] != s[right]) {
25 // Try deleting either the left character or the right character
26 // Check if remaining substring is palindrome after deletion
27 return isPalindrome(left + 1, right) || isPalindrome(left, right - 1);
28 }
29 // Move pointers towards center
30 left++;
31 right--;
32 }
33
34 // String is already a palindrome without any deletion
35 return true;
36 }
37};
38
1/**
2 * Determines if a string can be a palindrome after deleting at most one character
3 * @param s - The input string to check
4 * @returns true if the string is a valid palindrome (with at most one deletion), false otherwise
5 */
6function validPalindrome(s: string): boolean {
7 /**
8 * Helper function to check if a substring is a palindrome
9 * @param leftIndex - Starting index for comparison
10 * @param rightIndex - Ending index for comparison
11 * @returns true if the substring from leftIndex to rightIndex is a palindrome
12 */
13 const isPalindromeInRange = (leftIndex: number, rightIndex: number): boolean => {
14 // Compare characters from both ends moving towards the center
15 while (leftIndex < rightIndex) {
16 if (s[leftIndex] !== s[rightIndex]) {
17 return false;
18 }
19 leftIndex++;
20 rightIndex--;
21 }
22 return true;
23 };
24
25 // Initialize two pointers at the start and end of the string
26 let leftPointer: number = 0;
27 let rightPointer: number = s.length - 1;
28
29 // Compare characters from both ends
30 while (leftPointer < rightPointer) {
31 if (s[leftPointer] !== s[rightPointer]) {
32 // If mismatch found, try deleting either the left or right character
33 // Check if remaining substring is a palindrome after deletion
34 return isPalindromeInRange(leftPointer + 1, rightPointer) ||
35 isPalindromeInRange(leftPointer, rightPointer - 1);
36 }
37 leftPointer++;
38 rightPointer--;
39 }
40
41 // If no mismatches found, the string is already a palindrome
42 return true;
43}
44
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the string s
.
The algorithm uses a two-pointer approach starting from both ends of the string. In the main while loop, we traverse the string at most once with pointers i
and j
moving towards each other. When a mismatch is found at positions i
and j
, we call the helper function check()
twice at most - once for check(i, j-1)
and potentially once for check(i+1, j)
.
Each call to check()
also performs at most O(n)
comparisons in the worst case. However, the key insight is that:
- The main loop processes at most
n/2
characters before finding a mismatch (or completing if no mismatch) - When a mismatch is found,
check()
validates the remaining substring, which combined with the already processed part, totals to at mostn
character comparisons - The two
check()
calls are connected by anor
operator, so in the worst case we check at most2n
characters total
Therefore, the overall time complexity remains O(n)
.
Space Complexity: O(1)
.
The algorithm only uses a constant amount of extra space:
- Two pointers
i
andj
in the main function - Two local pointers
i
andj
in the helper functioncheck()
- No additional data structures are created
- The recursion depth is at most 1 (the helper function doesn't call itself recursively)
All variables use constant space regardless of the input size, resulting in O(1)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting Multiple Deletions
A common mistake is trying to continue checking after the first mismatch, potentially allowing multiple deletions.
Incorrect approach:
def validPalindrome(self, s: str) -> bool:
left, right = 0, len(s) - 1
deletion_used = False
while left < right:
if s[left] != s[right]:
if deletion_used: # Wrong: trying to track deletions manually
return False
# This approach fails because we don't know which character to skip
left += 1 # or right -= 1?
deletion_used = True
else:
left += 1
right -= 1
return True
Why it fails: When a mismatch occurs, we don't know whether to skip the left or right character without checking both possibilities.
2. Checking Only One Deletion Option
Some implementations only check one side when a mismatch occurs, missing valid palindromes.
Incorrect approach:
def validPalindrome(self, s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
# Only checking by skipping left character
return is_palindrome(left + 1, right)
left += 1
right -= 1
return True
Example failure: For s = "cbbcc"
, this would return False
when it should return True
(delete the first 'c').
3. Incorrect Helper Function Bounds
Off-by-one errors in the palindrome checking helper function.
Incorrect approach:
def is_palindrome(left: int, right: int) -> bool:
while left <= right: # Wrong: should be left < right
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
Why it matters: Using <=
instead of <
causes unnecessary comparison when pointers meet at the same index (which is always valid for odd-length palindromes).
4. Not Handling Edge Cases
Forgetting to handle strings of length 0, 1, or 2.
Solution: The correct implementation naturally handles these:
- Empty string or single character: The main while loop never executes, returns
True
- Two characters: Checked once, and if different, both deletion options lead to single characters (always palindromes)
5. Recursion Without Deletion Tracking
Using recursion without properly tracking whether a deletion has been used.
Incorrect approach:
def validPalindrome(self, s: str, deleted=False) -> bool:
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
if not deleted:
# Recursive calls with string slicing (inefficient)
return (self.validPalindrome(s[left+1:right+1], True) or
self.validPalindrome(s[left:right], True))
return False
left += 1
right -= 1
return True
Problems:
- String slicing creates new strings (space inefficient)
- Recursive approach adds unnecessary complexity
- Function signature doesn't match the expected interface
Correct Solution Reminder: The provided solution avoids all these pitfalls by:
- Using a helper function that checks palindromes on index ranges (no string copying)
- Checking both deletion options when a mismatch occurs
- Returning immediately after finding the first mismatch (ensuring at most one deletion)
- Using simple iteration instead of recursion
How does merge sort divide the problem into subproblems?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!