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 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 481class 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} 591class 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}; 381/** 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} 44Solution 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
itojis a palindrome - It uses two pointers moving towards each other
- Returns
Falseimmediately if any mismatch is found - Returns
Trueif 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 5-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!)
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/2characters 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 mostncharacter comparisons - The two
check()calls are connected by anoroperator, so in the worst case we check at most2ncharacters 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
iandjin the main function - Two local pointers
iandjin 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
What's the output of running the following function using the following tree as input? 
1def serialize(root): 2 res = [] 3 def dfs(root): 4 if not root: 5 res.append('x') 6 return 7 res.append(root.val) 8 dfs(root.left) 9 dfs(root.right) 10 dfs(root) 11 return ' '.join(res) 121import java.util.StringJoiner; 2 3public static String serialize(Node root) { 4 StringJoiner res = new StringJoiner(" "); 5 serializeDFS(root, res); 6 return res.toString(); 7} 8 9private static void serializeDFS(Node root, StringJoiner result) { 10 if (root == null) { 11 result.add("x"); 12 return; 13 } 14 result.add(Integer.toString(root.val)); 15 serializeDFS(root.left, result); 16 serializeDFS(root.right, result); 17} 181function serialize(root) { 2 let res = []; 3 serialize_dfs(root, res); 4 return res.join(" "); 5} 6 7function serialize_dfs(root, res) { 8 if (!root) { 9 res.push("x"); 10 return; 11 } 12 res.push(root.val); 13 serialize_dfs(root.left, res); 14 serialize_dfs(root.right, res); 15} 16Recommended 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!