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:
  - slug: two-pointers
    is_optimal: true

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)

    &nbsp;

    **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

    &nbsp;

    **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`

    &nbsp;

    **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`

    &nbsp;

    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.
