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", return true (already a palindrome, no deletion needed)
  • If s = "abca", return true (delete 'c' to get "aba" which is a palindrome)
  • If s = "abc", return false (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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Skip the left character ('b') and check if the remaining part is a palindrome
  2. 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 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

Solution Approach

The implementation uses the two-pointer technique to efficiently solve this problem.

Main Algorithm Structure:

  1. Helper Function check(i, j):

    • This function verifies if the substring from index i to j 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
  2. Main Logic:

    • Initialize two pointers: i = 0 (left) and j = len(s) - 1 (right)
    • Move pointers towards each other comparing characters at each step

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 position j (right pointer)
  • check(i + 1, j): Skip the character at position i (left pointer)

We use the logical or operator because we only need one of these options to work.

Example Walkthrough with s = "abca":

  1. Compare s[0]='a' with s[3]='a' → match, move pointers
  2. Compare s[1]='b' with s[2]='c' → mismatch
  3. Try check(1, 1) (skip right 'c'): checks if "b" is palindrome → True
  4. 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 Evaluator

Example Walkthrough

Let's walk through the solution with s = "raceacar".

Initial Setup:

  • String: "raceacar" (indices 0-7)
  • Left pointer i = 0, Right pointer j = 7

Step-by-step Execution:

  1. First comparison: s[0]='r' vs s[7]='r'

    • They match! Move pointers inward
    • Update: i = 1, j = 6
  2. Second comparison: s[1]='a' vs s[6]='a'

    • They match! Move pointers inward
    • Update: i = 2, j = 5
  3. Third comparison: s[2]='c' vs s[5]='c'

    • They match! Move pointers inward
    • Update: i = 3, j = 4
  4. Fourth comparison: s[3]='e' vs s[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
  5. Final Result: Since check(4, 4) returns True, the function returns True

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/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 most n character comparisons
  • The two check() calls are connected by an or operator, so in the worst case we check at most 2n 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 and j in the main function
  • Two local pointers i and j in the helper function check()
  • 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
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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) 12
1import 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} 18
1function 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} 16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More