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 function_signature: "def valid_palindrome(s: str) -> bool:" test_cases: visible: - input: { s: "aba" } expected: true - input: { s: "abca" } expected: true - input: { s: "abc" } expected: false hidden: - input: { s: "a" } expected: true - input: { s: "aa" } expected: true - input: { s: "ab" } expected: true - input: { s: "racecar" } expected: true - input: { s: "racecars" } expected: true 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.