Skip to content

125. Valid Palindrome (Easy)

Problem

Given a string s, return true if it reads the same forwards and backwards after removing non-alphanumeric characters and lowercasing the rest.

Examples

  • s = "A man, a plan, a canal: Panama"true
  • s = "race a car"false
  • s = " "true (empty after cleaning)

LeetCode 125 · Link · Easy

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, clean, then compare to reverse

Filter to alphanumerics, lowercase, and compare to the reversed string.

def is_palindrome(s: str) -> bool:
cleaned = "".join(ch.lower() for ch in s if ch.isalnum()) # L1: O(n) filter+join
return cleaned == cleaned[::-1] # L2: O(n) reverse+compare

Where the time goes, line by line

Variables: n = len(s).

LinePer-call costTimes executedContribution
L1 (filter + join)O(n)O(n)1O(n)O(n) ← dominates
L2 (reverse + compare)O(n)O(n)1O(n)O(n)

Both steps are linear; neither dominates asymptotically.

Complexity

  • Time: O(n)O(n), driven by L1/L2 (two linear passes).
  • Space: O(n)O(n). The cleaned string and its reverse.

Direct and clear, but uses an extra O(n)O(n) allocation where O(1)O(1) is achievable.

Approach 2: Clean, then two pointers

Build the cleaned string, then walk from both ends. Same asymptotics as Approach 1 with an early-exit on the first mismatch.

def is_palindrome(s: str) -> bool:
cleaned = [ch.lower() for ch in s if ch.isalnum()] # L1: O(n) filter
l, r = 0, len(cleaned) - 1 # L2: O(1) init pointers
while l < r: # L3: at most n/2 iterations
if cleaned[l] != cleaned[r]: # L4: O(1) compare
return False # L5: O(1) early exit
l, r = l + 1, r - 1 # L6: O(1) advance both
return True

Where the time goes, line by line

Variables: n = len(s).

LinePer-call costTimes executedContribution
L1 (filter)O(n)O(n)1O(n)O(n) ← dominates
L2 (init pointers)O(1)O(1)1O(1)O(1)
L3-L6 (two-pointer scan)O(1)O(1)at most n/2O(n)O(n)

The filter pass dominates; the pointer scan is at most half the cleaned length.

Complexity

  • Time: O(n)O(n), driven by L1 (filter) and L3/L4 (scan). Best case O(1)O(1) (first mismatch).
  • Space: O(n)O(n). The cleaned list.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Two pointers, skip non-alphanumerics in-place (optimal)

Walk both ends of the original string, skipping over non-alphanumeric characters on the fly. No extra allocation.

def is_palindrome(s: str) -> bool:
l, r = 0, len(s) - 1 # L1: O(1) init pointers
while l < r: # L2: at most n steps total
while l < r and not s[l].isalnum(): # L3: skip non-alnum on left
l += 1 # L4: O(1)
while l < r and not s[r].isalnum(): # L5: skip non-alnum on right
r -= 1 # L6: O(1)
if s[l].lower() != s[r].lower(): # L7: O(1) compare
return False # L8: O(1) early exit
l, r = l + 1, r - 1 # L9: O(1) advance both
return True

Where the time goes, line by line

Variables: n = len(s).

LinePer-call costTimes executedContribution
L1 (init)O(1)O(1)1O(1)O(1)
L2 (outer loop)O(1)O(1)at most nO(n)O(n)
L3-L6 (skip non-alnum)O(1)O(1) per stepn totalO(n)O(n) ← dominates
L7-L9 (compare + advance)O(1)O(1)at most n/2O(n)O(n)

Each character in s is touched at most once (either skipped in L3/L5 or compared in L7). Total work is O(n)O(n).

Complexity

  • Time: O(n)O(n), driven by L3-L6 (each character visited once). Each index is visited at most once.
  • Space: O(1)O(1). No auxiliary structures.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
Clean + reverse compareO(n)O(n)O(n)O(n)
Clean + two pointersO(n)O(n)O(n)O(n)
Two pointers in placeO(n)O(n)O(1)O(1)

Same Big-O on time, but the in-place two-pointer version is the canonical “optimal-space” answer.

Test cases

# Quick smoke tests, paste into a REPL or save as test_valid_palindrome.py and run.
# Uses the canonical implementation (Approach 3: two pointers in place).
def is_palindrome(s: str) -> bool:
l, r = 0, len(s) - 1
while l < r:
while l < r and not s[l].isalnum():
l += 1
while l < r and not s[r].isalnum():
r -= 1
if s[l].lower() != s[r].lower():
return False
l, r = l + 1, r - 1
return True
def _run_tests():
assert is_palindrome("A man, a plan, a canal: Panama") == True
assert is_palindrome("race a car") == False
assert is_palindrome(" ") == True
assert is_palindrome("") == True
assert is_palindrome("a") == True
assert is_palindrome("aa") == True
assert is_palindrome("ab") == False
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Strings, input; in-place traversal with isalnum
  • Two Pointers, the two index invariant that shrinks or coordinates positions without nested loops.
  • Array Scans, the linear pass habit of carrying just enough state while reading each item once.