DSA sheet · Strings · Two Pointers pattern

Valid Palindrome II

This is the next two-pointer string question after Valid Palindrome. The teacher warns that it looks easy but isn't: one extra line, "you may delete at most one character", changes the problem. The video shows the O(n²) brute force (try deleting every character), explains why the constraints reject it, and then builds an O(n) solution: walk two pointers inwards, and at the first mismatch try exactly two options, skip the left character or skip the right one. She also clears up a common confusion: this is not dynamic programming. It's asked in interviews, so the reasoning matters more than the few lines of code.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting: two pointers from scratch

Pointers are just indexes

A pointer here is an index into the string: "I'm looking at position i". Two pointers means we keep two indexes and move them by rules, instead of running two nested loops.

The brute force two pointers usually replaces

Trying every pair (i, j) with two loops costs O(n²). With n = 10⁵ that's about 10¹⁰ steps, way past the rough 10⁸ operations safe limit → TLE. Two pointers helps when the input lets us skip most pairs. In a sorted array, a sum that's too big tells us to move the right pointer left. In a palindrome, index i only ever needs its mirror index n − 1 − i, so the other pairs never need checking.

The two-pointer family (you'll meet all of these in the notebook)

kindmovementexample
Opposite endsleft from index 0, right from the last index, walking towards each other until they meet/crossValid Palindrome I & II, Container With Most Water
Same direction (slow/fast, read/write)fast reads every element, slow marks where the next kept element goesMove Zeroes
Three pointerslow / mid / high split the array into 0s, 1s, unknown, 2sSort Colors
Fix one + two pointersa loop fixes i, opposite-end pointers search the rest; skip equal neighbours to avoid duplicates3Sum
Left-max / right-maxopposite ends, each side remembers its tallest bar so farTrapping Rain Water
Palindromes(a) compare from both ends inwards; (b) expand around a center, odd (one character) or even (between two characters)this page uses (a); Longest Palindromic Substring uses (b)

Why two pointers and not sliding window (the teacher's rule)

Strings have two main patterns. Sliding window is for when we need everything in a continuous block between two ends. Two pointers is for when we compare exactly two points, then move them and compare the next two. A palindrome check compares the front character with the back one, nothing in between, so it's two pointers.

The basic palindrome check (from Valid Palindrome)

left = 0, right = n − 1. While left < right: if s[left] != s[right] → not a palindrome. Otherwise move both inwards. If the loop ends, it's a palindrome. We stop at left < right because when both point at the same middle character, it trivially equals itself. We'll reuse this check, but on a part of the string given by two indexes.

Words

Part A · Brute force: try deleting every character

LeetCode 680

1The question in simple words

Given a string s (only lowercase letters), return True if you can make it a palindrome by deleting at most one character. Otherwise return False.

swhat happensanswer
"aba"already a palindrome, delete nothing (zero deletions is allowed)True
"abca"not a palindrome; delete c → "aba" (or delete b → "aca")True
"abcda"delete c → "abda" ✗, delete d → "abca" ✗, any other single deletion also failsFalse

2What the constraints tell us

3Intuition

"At most one" means two things to check, in order:

  1. Zero deletions: is s already a palindrome? If yes → True.
  2. One deletion: delete each character one at a time, and test whether what's left is a palindrome. If any works → True.

For the palindrome test, the brute force uses the plain definition: reverse the string into a copy s1 and compare position by position.

4Building the logic from the example

Take s = "abca".

The test itself is a small function: given s and its reverse s1 (same length n), loop i from 0 to n − 1. If s[i] != s1[i] → return False. After the loop → return True.

Doubt: each check is O(n). So isn't the whole brute force O(n), or 2n?
→ No, and this is the teacher's key point. One check is O(n) to copy/reverse plus O(n) to compare. But we may run that check once for every deleted position, which is n times. Worst case: a long string like abcdefg…zabc… that never becomes a palindrome. Every deletion is tried and every one fails. So it's n checks × O(n) each = O(n²).

5Approach steps

  1. If s equals its reverse → return True (zero deletions).
  2. For each index i from 0 to n − 1: build t = s without index i.
  3. If t equals its reverse (checked char by char) → return True.
  4. If no deletion worked → return False.

6Code (Python)

Valid Palindrome II, brute force O(n²), too slow for n = 10⁵
class Solution:
    def validPalindrome(self, s: str) -> bool:
        def same(a, b):                      # a and b have equal length
            for i in range(len(a)):
                if a[i] != b[i]:
                    return False
            return True

        if same(s, s[::-1]):                 # zero deletions
            return True
        for i in range(len(s)):              # try deleting each index
            t = s[:i] + s[i + 1:]
            if same(t, t[::-1]):
                return True
        return False

7Code line by line

linewhat it means
def same(a, b):The teacher's helper: compare a string with its reversed copy, position by position.
if same(s, s[::-1]): return True"At most one" includes zero. Check the original string first.
t = s[:i] + s[i + 1:]Build a new string with index i removed. This alone is O(n) work and O(n) memory.
if same(t, t[::-1]): return TrueReverse the shorter string and compare. One success is enough.
return FalseNo single deletion helped.

8Dry run (hand table)

s = "abcda" (the False example):

deleted indextreverse(t)first differenceresult
noneabcdaadcbaposition 1: b vs d✗
0 (a)bcdaadcbposition 0: b vs a✗
1 (b)acdaadcaposition 1: c vs d✗
2 (c)abdaadbaposition 1: b vs d✗
3 (d)abcaacbaposition 1: b vs c✗
4 (a)abcddcbaposition 0: a vs d✗

All failed → False. We did 6 full checks on a 5-letter string. That's the n × n growth.

9Complexity & remember

Remember the brute forceCheck zero deletions, then delete each index and re-check. Correct but O(n²). The constraints rule it out.

Part B · Optimal: two pointers + two tries at the first mismatch

1The question again, with the new goal

Same question. Goal: O(n) time, no copies of the string (O(1) extra space).

2What the constraints push us towards

n up to 10⁵ means O(n) or O(n log n). The brute force repeats almost the same palindrome check n times. The idea is to do the check once, and only "branch" at the one place where things go wrong.

3Intuition: walk until something breaks

Run the normal two-pointer palindrome check from both ends. As long as the characters match, nothing needs deleting, so keep walking inwards. At the first mismatch, s[left] != s[right], we know one of these two characters is the troublemaker. We're allowed to throw away just one of them, so try both:

If either is a palindrome → True. If neither → False. And if we never hit a mismatch, the string was already a palindrome (zero deletions) → True.

4Building the conditions from examples

Example 1: "abca"

startabcaa = a → move both
L  R
mismatchabcab ≠ c → try skipping b, try skipping c
 LR 

Skip b → the window is just "c", a single character, so it's a palindrome → True (the whole string becomes "aca"). Skip c → window "b" would also be True. Either way the answer is True.

Example 2 (the teacher's second example): "afgfea"

startafgfeaa = a → move both
L    R
mismatchafgfeaf ≠ e → two tries
 L  R 

One of them is True, so the answer is True. Combine the two tries with or, because we need at least one to work, not both.

Condition: inside a try, no more deletions

When we test the window after skipping one character, we've already used our one deletion. So inside that check, the first mismatch means False immediately. Don't try to skip again; that would be a second deletion.

Condition: don't copy the string, pass indexes

The brute force built new strings. Here the helper takes the original s plus two indexes l and r, and checks only that window in place. Passing the whole s is fine, since the helper only ever looks between l and r. No s1, no extra space.

Condition: while left < right (in both loops)

Same as Valid Palindrome: when left = right both are on one character, which matches itself, so there's nothing to check.

Condition: on a mismatch, return the result of the tries

In the main loop, a mismatch does not mean False right away, because we still have one deletion. Instead we return whatever the two tries give. The main loop never continues after that point.

Doubt: can't I just peek one step ahead, e.g. "if s[left + 1] == s[right], skip left, otherwise skip right", and try only one side?
→ No. Both sides can look fine for one step and only one works all the way. Take "abbab": the first pair is a vs b, a mismatch. s[left+1] = b equals s[right] = b, and s[left] = a equals s[right−1] = a. Skipping left gives "bbab" ✗. Skipping right gives "abba" ✓. A one-step peek that picks "skip left" would wrongly say False. So we fully check both. It costs only O(n) more.
Doubt: is this dynamic programming? It "tries two options".
→ The teacher says clearly: no. DP is for problems with many overlapping subproblems that we store and reuse. Here we branch once (only one deletion is allowed), each branch is a simple straight check, and nothing is repeated or stored. Call it two pointers with a small greedy step: keep matching pairs without deleting, and branch only at the first conflict.

5Approach steps

  1. Write a helper check(s, l, r): while l < r, if s[l] != s[r] → False, else move both inwards. After the loop → True.
  2. Main: left = 0, right = len(s) − 1.
  3. While left < right:
  4.   if s[left] != s[right] → return check(s, left + 1, right) or check(s, left, right − 1);
  5.   else → left += 1, right -= 1.
  6. After the loop (no mismatch at all) → return True.

6Code (Python)

Valid Palindrome II, two pointers, O(n) time, O(1) space
class Solution:
    def validPalindrome(self, s: str) -> bool:
        left, right = 0, len(s) - 1
        while left < right:
            if s[left] != s[right]:
                # one deletion allowed: skip the left char OR skip the right char
                return (self.isPalindromeRange(s, left + 1, right) or
                        self.isPalindromeRange(s, left, right - 1))
            left += 1
            right -= 1
        return True                          # no mismatch: zero deletions needed

    def isPalindromeRange(self, s, l, r):
        while l < r:
            if s[l] != s[r]:
                return False                 # deletion already used, so fail
            l += 1
            r -= 1
        return True

7Code line by line

linewhat it means
left, right = 0, len(s) - 1Fingers on the first and last characters.
while left < right:Compare mirror pairs until the fingers meet.
if s[left] != s[right]:First conflict. One of these two must be deleted (Part C proves why).
self.isPalindromeRange(s, left + 1, right)Try 1: pretend s[left] is deleted. Is everything between left + 1 and right a palindrome?
or self.isPalindromeRange(s, left, right - 1)Try 2: pretend s[right] is deleted. Python's or runs this only if try 1 was False.
return (…)The answer is decided here. Pairs outside the window already matched, so only the window matters.
left += 1 right -= 1The pair matched, no deletion needed. Move both inwards.
return TrueThe loop finished without a conflict → already a palindrome.
def isPalindromeRange(s, l, r)The plain Valid Palindrome check, limited to the window [l, r], with no skipping allowed.

8Dry run

s = "afgfea" (indexes 0–5)

stepwhereleftrights[left]s[right]decisionresult
1main05aasame → move bothL=1, R=4
2main14femismatch → try both skips–
3try 1: [2, 4]24gediffer → no more deletionsFalse
4try 2: [1, 3]13ffsame → move bothl=2, r=2
5try 222ggl < r false → loop endsTrue
6mainFalse or Truereturn True
try 1afgfeaf skipped · g vs e ✗
  L R 
try 2afgfeae skipped · f = f, then the middle g → ✓
 L R  

s = "abcda" (the False case)

mismatchabcdaa = a matched; now b ≠ d
 L R 
stepwhereleftrights[left]s[right]decisionresult
1main04aasame → move bothL=1, R=3
2main13bdmismatch → try both–
3try 1: [2, 3]23cddifferFalse
4try 2: [1, 2]12bcdifferFalse
5mainFalse or Falsereturn False

Compare with Part A: there we needed 6 full checks for this string. Here we made 2 main comparisons and 2 short tries.

s = "aba" (zero deletions)

a = a → move both → left = right = 1 → loop ends → True. The helper is never called.

9Complexity & remember

Remember Valid Palindrome II Walk from both ends. Match → move both. First mismatch → return check(l+1, r) or check(l, r−1), where the check allows no more deletions. No mismatch → True. Not DP: one branch only.

Part C · Why trying both skips is enough

The code tries only 2 deletions instead of n. Why can't the right deletion be somewhere else? Here's the reasoning in three small steps.

1Where we are at the first mismatch

  s = [ x x x | s[l] . . . . . s[r] | x x x ]
        ^^^^^                         ^^^^^
        outer left part   ==   outer right part (mirrored), already matched
                       window [l, r], and s[l] != s[r]

Everything outside the window matched in mirror pairs. Inside, the two ends differ.

2We must delete exactly one character

s itself isn't a palindrome (s[l] ≠ s[r] are a mirror pair), so zero deletions won't work. Exactly one deletion is needed.

3Deleting something strictly inside the window can't work

Say we delete a character between l and r (not l, not r). The outer parts are untouched and still match, so in the new string the mirror pairs line up the same way, and s[l] and s[r] are still a mirror pair. They're still different, so it's not a palindrome. So the deleted character must be s[l] or s[r] itself. Those are exactly our two tries.

4What about deleting from the outer matched part?

Could it help to delete a character left of l (or right of r)? Then everything shifts by one, so it's less obvious. But here's the trick: if deleting an outer character at k < l gives a palindrome, the mirror matches force s[k] = s[k+1] = … = s[l] (each shifted character must equal its neighbour). In other words, k sits in a run of equal letters that reaches all the way to l. And deleting any one letter from a run of equal letters gives the same string. So deleting k gives exactly the same result as deleting l, which is our try 1. The right side works the same way and matches try 2.

Doubt: show me that with a real string.
→ s = "aaba". The first pair a = a matches. Then left = 1 (a), right = 2 (b): a mismatch. Deleting index 0 (an outer character) gives "aba" ✓. But index 0 and index 1 are both "a", a run, so deleting index 1 gives the same "aba". Try 1 (skip left, window [2, 2] = "b") finds it ✓.

5Why the greedy "match → move on" is safe

Steps 3 and 4 also show that while pairs keep matching, we never need to delete one of them. Any useful deletion out there is equivalent to deleting at the first mismatch. So walking inwards without branching on matches loses nothing. That's the "slightly greedy" part the teacher mentions.

6Why we fully check both, not just one

Both ends are candidates, and a one-step peek can't tell which is right (the "abbab" example in Part B). Checking both fully costs only about 2 × n/2, so it's still O(n).

The proof in one breath Outer pairs already match → the window's ends s[l] ≠ s[r] must stop being a pair → one of them has to go (an outer deletion is the same as one of these, via a run of equal letters) → test both windows, each with no further deletions.

The test file also checks this by comparing against the brute force on every string of a, b, c up to length 8.


Part D · Revision page

Brute force (Part A)Two pointers (Part B)
ideacheck s; then delete every index and check againwalk inwards; branch once at the first mismatch
palindrome testreverse into a copy, compareindexes l, r on the same string
deletions triedall nonly 2 (skip left, skip right)
timeO(n²) → 10¹⁰ for n = 10⁵ → TLEO(n) (about 3n/2)
extra spaceO(n)O(1)
Valid Palindrome IValid Palindrome II
charactersmixed: skip non-alnum, compare lowercaseonly lowercase letters
on mismatchreturn Falsereturn try(l+1, r) or try(l, r−1)
loop / stopwhile left < right; match → move both; True after the loop
If you remember only 5 lines 1. "At most one" includes zero: an untouched palindrome is True.
2. Two pointers from both ends; on a match, move both.
3. First mismatch: one of s[l], s[r] must go, so test [l+1, r] and [l, r−1].
4. Combine with or; inside a try, no more deletions.
5. O(n) time (≈ 3n/2), O(1) space; it's greedy two pointers, not DP.
Mistakes to avoid ✗ returning False at the first mismatch (one deletion is allowed)
✗ trying only one side, or picking a side by peeking one character ("abbab")
✗ allowing a second skip inside the helper
✗ using and instead of or for the two tries
✗ slicing new strings for every try (extra O(n) space)
✗ forgetting the zero-deletion case in the brute force
test it yourself (paste under any of the solutions above)
s = Solution()
print(s.validPalindrome("aba"))      # True  (zero deletions)
print(s.validPalindrome("abca"))     # True  (delete b or c)
print(s.validPalindrome("abc"))      # False
print(s.validPalindrome("abcda"))    # False
print(s.validPalindrome("afgfea"))   # True  (delete e)
print(s.validPalindrome("abbab"))    # True  (delete the last b)
print(s.validPalindrome("a"))        # True
print(s.validPalindrome("ab"))       # True  (delete either)

Based on this video: Valid Palindrome II | Two Pointers