HARD Lab · Strings · 16

125. Valid Palindrome

Ignore punctuation and case: a string is a valid palindrome if its alphanumeric characters read the same forwards and backwards. One inward pass with two pointers — no extra allocation — decides it in O(n).

EasyTwo PointersTypeScript

PROBLEM What we're solving

Given a string s, return true if it is a palindrome after converting all uppercase letters to lowercase and removing all non-alphanumeric characters. "A man, a plan, a canal: Panama" true (filtered: amanaplanacanalpanama). "race a car" false (raceacar ≠ its reverse).

KEY IDEA Skip junk, compare the rest from both ends

Insight →you never need to build a cleaned string. Place one pointer at each end, advance past any non-alphanumeric character, then compare the lowercased pair. If they ever differ, it's not a palindrome. If the pointers cross without a mismatch, it is.

RECIPE Two pointers, skip and compare

  • 0 · Set up. l = 0, r = s.length − 1. We narrow inward.
  • 1 · Skip left junk. While s[l] is not alphanumeric, increment l. This filters spaces, punctuation, and symbols without allocating a new string.
  • 2 · Skip right junk. Same idea: advance r leftward past non-alphanumeric characters.
  • 3 · Compare. If s[l].toLowerCase() !== s[r].toLowerCase(), return false immediately.
  • 4 · Advance. Move l++ and r-- and repeat from step 1.
  • 5 · Done. If l ≥ r, every alphanumeric pair matched — return true.
Classic confusion → the skip loops use l < r as their guard, not just l < s.length. Without this guard, both pointers can walk past each other for strings that are all non-alphanumeric (e.g. " "), and you'd compare out-of-bounds indices.

COST Complexity & alternatives

Filter + reverse string
O(n)
O(n) time but also O(n) space for the cleaned copy.
Two pointers in-place
O(n)
O(n) time, O(1) space — no auxiliary string.

Space note

The clever solution has the same asymptotic time but eliminates the hidden constant of building a cleaned string. The regex check per character is O(1) because the character set is bounded.

Pattern transfer → the same skip-and-compare idea powers Valid Palindrome II (allow one deletion), Palindrome Linked List (slow/fast pointers then compare), and Longest Palindromic Substring (expand from center). Any time you need to check symmetry on a sequence, two pointers from the ends are the first tool to reach for.

RUN IT Two pointers meet in the middle

step 0 / 20
STARTTwo pointers start at both ends. Skip non-alphanumeric chars; compare lowercased letters.
stringa0 1m2a3n4,5 6a7 8p9l10a11n12,13 14a15 16c17a18n19a20l21:22 23p24a25n26a27m28a29
State
0
l
29
r
active pointermismatchpalindrome confirmed
slowfast

TYPESCRIPT The solution, annotated

isPalindrome.ts
function isPalindrome(s: string): boolean {
  let l = 0;
  let r = s.length - 1;

  while (l < r) {
    // skip non-alphanumeric from left
    while (l < r && !isAlphanumeric(s[l])) l++;
    // skip non-alphanumeric from right
    while (l < r && !isAlphanumeric(s[r])) r--;

    if (s[l].toLowerCase() !== s[r].toLowerCase()) return false;
    l++;
    r--;
  }
  return true;
}

function isAlphanumeric(ch: string): boolean {
  return /[a-zA-Z0-9]/.test(ch);
}

Reading it block by block

Lines 2–3 — initialize pointers. l starts at the first character, r at the last. The outer while (l < r) loop narrows the window until the pointers meet or cross.
Lines 6–9 — skip non-alphanumeric characters. Two inner while loops advance each pointer past spaces, punctuation, and symbols. The l < r guard inside these loops is essential — it prevents overrun when the remaining window is exhausted or contains no alphanumeric characters.
Line 11 — compare the pair. Both characters are lowercased before comparison so case is ignored. A mismatch immediately returns false — no need to continue.
Lines 12–13 — advance inward. After a successful match, both pointers move one step closer to the center. The outer loop condition l < r ends the search once the window collapses.
Line 15 — palindrome confirmed. Exiting the loop without returning false means every alphanumeric pair matched — the string is a valid palindrome.
Lines 19–21 — helper. isAlphanumeric uses a simple regex. For performance-critical code you could use char-code arithmetic instead, but the regex is clearer and interviewers prefer readable code.
Complexity → O(n) time — each character is visited at most once by each pointer. O(1) space — only two integer indices; no cleaned string is ever allocated.

INTERVIEWFollow-ups they'll ask

  • "What if one deletion is allowed?" This is Valid Palindrome II: on a mismatch, try skipping s[l] or s[r] and check if either remainder is a palindrome.
  • "What's the brute-force, and why is this better?" Strip non-alphanumeric characters into a new string and compare it to its reverse — O(n) time but O(n) space. Two pointers keep space O(1).
  • "How does this handle the empty string?" With l = 0 and r = -1 the loop condition l < r is immediately false, so it returns true — correct, since an empty string is vacuously a palindrome.
  • "Can you do it without regex?" Yes: check (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') on the lowercased character. Avoids regex overhead in tight loops.
  • "How would you handle Unicode / multi-byte characters?" Use spread or Array.from(s) to get code-point–safe iteration and a Unicode-aware regex like /\p{L}|\p{N}/u.

MNEMONIC The one-liner

"Squeeze from both ends — skip the noise, compare the letters, stop when they meet."

TRIGGERS When you see ___ → reach for ___

"reads the same forwards and backwards"two pointers from ends
ignore spaces / punctuation / caseskip non-alphanumeric in the pointer loop
check symmetry without extra memoryin-place two pointers, no new string
"allow one deletion"palindrome → branch on mismatch (Valid Palindrome II)

SKELETON The reusable shape

skeleton.ts
function isPalindrome(s: string): boolean {
  let l = 0, r = s.length - 1;
  while (l < r) {
    while (l < r && !isAlphanumeric(s[l])) l++;
    while (l < r && !isAlphanumeric(s[r])) r--;
    if (s[l].toLowerCase() !== s[r].toLowerCase()) return false;
    l++;
    r--;
  }
  return true;
}

FLASHCARDS Tap to flip

What are the two stopping conditions for the outer loop?
l >= r (pointers crossed — palindrome) or an explicit return false on mismatch.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the space complexity of the two-pointer approach?
QUESTION 02
Trace "race a car" — what is the first mismatch?
QUESTION 03
Why must the inner skip-loops guard with l < r, not just l < s.length?
QUESTION 04
What does the algorithm return for s = " " (a single space)?
QUESTION 05
Which of these is the best description of when to reach for two-pointer palindrome check?
QUESTION 06
How would you extend this to allow at most one character deletion (Valid Palindrome II)?
QUESTION 07
After filtering "A man, a plan, a canal: Panama", what is the resulting lowercase alphanumeric string?