Valid Palindrome

If you can derive this solution from first principles, you’ve mastered the foundations of opposing two-pointers.


Pattern Summary

This problem uses the [Two Pointers] pattern, specifically the opposing directional subgroup.

  • Recognition: Checking symmetry or finding pairs from opposite ends of a sequence.
  • Core Invariant: The left pointer strictly monotonically increases; the right pointer strictly monotonically decreases. They meet in the middle.
  • Transformation: Replace $O(n)$ auxiliary space (building a cleaned string) with $O(1)$ in-place noise skipping.

$\rightarrow$ Read the full theoretical foundation: [Two Pointers]


Real-World Analogues

Data Sanitization

Zero-Allocation Stream Filtering
When processing incoming telemetry data streams on embedded devices with tight memory constraints, allocating new buffers to “clean” the data is prohibited. Instead, pointers are used to validate or read the stream by skipping noise bytes on the fly.

Syntax Validation

Symmetrical Packet Validation
Validating the structural symmetry of packet headers or sequence markers without copying the buffer into memory.


Interview Abstraction

LeetCode 125 asks the foundational opposing pointers question: given a string s, return true if it reads the same forward and backward after removing all non-alphanumeric characters and converting to lowercase, in $O(n)$ time and $O(1)$ space.


Why This Problem Matters

This problem isolates one foundational idea: in-place opposing traversal with noise filtering.

It is the purest environment to master bounds checking and guard clauses before moving to more complex monotonic opposing pointers like 3Sum or Container With Most Water.

What This Problem TeachesWhat It Does NOT Teach
✓ Opposing pointer convergence✗ Greedy area optimization (see Container With Most Water)
✓ Amortized $O(1)$ bounds checking✗ Sliding windows (see Continuous Subarrays)
✓ In-place zero-allocation filtering✗ Fast/Slow cycle detection (see Linked List Cycle)

Recognition

Reach for this pattern when:

  • ✓ You need to evaluate symmetry or pairs from the outside in.
  • ✓ The sequence is implicitly or explicitly ordered/structured for convergence.
  • ✓ Allocating a new cleaned array/string is flagged as sub-optimal.

The Mental Shift

Naive Thought   → "Clean the string first, then reverse it and compare."
Correct Thought → "Compare characters on the fly, skipping the noise as we go."

By delaying the “cleaning” step until the exact moment of comparison, you avoid allocating an entirely new string in memory.


Solution Evolution

ApproachTimeSpaceCore IdeaWhy It Fails / Bottleneck
1. Brute Force (Clean & Reverse)$O(n)$$O(n)$Filter to a new string, return clean == clean[::-1]Allocates $O(n)$ extra memory; unnecessary overhead.
2. Opposing Pointers$O(n)$$O(1)$Converge from ends, skip noise on the flyOptimal; strictly constant space and single pass.

Why This Beats the Brute Force

The brute force answers: “What does the completely cleaned string look like?”
The two-pointer approach answers: “Are the outermost valid characters matching right now?”

That single distinction eliminates the need for allocating any extra memory.


From Pattern → Code

The opposing pointers pattern maps to simple policies:

# 1. SKIP: Advance left pointer past invalid characters (using guard clauses)
if not s[left].isalnum():
    left += 1
    continue

# 2. COMPARE: Ensure valid characters match
if s[left].lower() != s[right].lower():
    return False

# 3. CONVERGE: Move both pointers inward
left += 1
right -= 1

Optimal Solution

def is_palindrome(s: str) -> bool:
    left, right = 0, len(s) - 1
    
    while left < right:
        # Guard clause: skip non-alphanumerics from the left
        if not s[left].isalnum():
            left += 1
            continue

        # Guard clause: skip non-alphanumerics from the right
        if not s[right].isalnum():
            right -= 1
            continue

        # Core logic: compare the valid characters
        if s[left].lower() != s[right].lower():
            return False

        # Convergence
        left += 1
        right -= 1

    return True

Visual Walkthrough

Tracing s = "race a car".


Complexity Analysis

MetricComplexityRationale
Time$O(n)$Every character is evaluated at most once as the left and right pointers converge.
Space$O(1)$Only two integer pointers are used, regardless of the size of the input string.

Edge Cases

DimensionTest CaseExpected Behavior
Empty/Spacess = " "Pointers cross without making any comparisons; returns True.
Single Characters = "a"left < right is 0 < 0 (False); loop doesn’t run, returns True.
No Alphanumericss = ".,!?"All characters skipped, pointers cross, returns True.
Mixed Cases = "Aa"lower() standardizes comparison; returns True.

Common Mistakes

  • Nested While Loops Without Bounds Checks: If you use inner while loops to skip characters instead of continue guard clauses, forgetting to re-check left < right inside the inner loop will cause IndexError.
  • Allocating Memory: Using list comprehensions like [c.lower() for c in s if c.isalnum()] defeats the purpose of the two-pointer optimization by consuming $O(n)$ space.

Problem Progression

Mastering this problem enables you to solve a progression of harder variations:

  1. Valid Palindrome II (Easy): Allow at most one character deletion.
  2. Two Sum II (Medium): Find a target sum in a sorted array using opposing pointers.
  3. Container With Most Water (Medium): Greedy opposing pointer convergence to maximize area.
  4. 3Sum (Medium): Combine iteration with opposing pointers for triplets.

Principal Lens

If you remember one thing: Avoid cleaning data in bulk if you only need to process it from the edges. Guard clauses allow you to filter noise on the fly, reducing complex nested loops into flat, strictly-bounded increments.