RezloRezloPrep
On this page

Valid Palindrome

7 min read
Check whether a string is a palindrome after normalizing it - without ever building the normalized string - by skipping non-alphanumeric characters directly on two pointers walking inward.

Problem Statement

A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward.

Alphanumeric characters include letters (a-z, A-Z) and numbers (0-9).

Given a string s, return true if it is a palindrome, or false otherwise.

Example 1:
Input: s = "A man, a plan, a canal: Panama"
Output: true
Explanation: After removing spaces, commas, and the colon, the string reads the same in both directions.
Example 2:
Input: s = "race a car"
Output: false
Explanation: The characters do not read the same forward and backward.
Example 3:
Input: s = " "
Output: true
Explanation: After removing the space, nothing remains, and an empty string is considered a palindrome.

Brute Force

Create a new string containing only alphanumeric characters, convert it to lowercase, then compare it with its reversed version.

It works, but it uses O(n) extra space to store the cleaned string.

function isPalindrome(s) {
const cleaned = s.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
const reversed = cleaned.split("").reverse().join("");
return cleaned === reversed;
}

Time: O(n)    Space: O(n) — the cleaned and reversed strings require extra memory.

Better Approach: Two Pointers

Instead of creating a cleaned string, use two pointers directly on the original string.

Put one pointer at the start and one at the end.

  • If left points to a non-alphanumeric character, move it forward.
  • If right points to a non-alphanumeric character, move it backward.
  • Once both pointers point to valid characters, compare them ignoring case.
  • If they don't match, return false.
  • Otherwise, move both pointers inward.
function isPalindrome(s) {
let left = 0;
let right = s.length - 1;
const isAlphaNumeric = (ch) => /[a-zA-Z0-9]/.test(ch);
while (left < right) {
if (!isAlphaNumeric(s[left])) {
left++;
} else if (!isAlphaNumeric(s[right])) {
right--;
} else {
if (s[left].toLowerCase() !== s[right].toLowerCase()) {
return false;
}
left++;
right--;
}
}
return true;
}

Complexity

Time: O(n)

Every character is visited at most once as the two pointers move toward the middle.

Space: O(1)

Only two pointers and a few variables are used. No cleaned or reversed string is created.

How to Explain It in an Interview

How to Explain it in an Interview

The 30-Second Spoken Pitch
"I use two pointers, one at the start and one at the end. If either pointer is on a non-alphanumeric character, I skip it. Once both pointers point to valid characters, I compare them case-insensitively. If they don't match, I return false. Otherwise, I move both pointers inward and continue. If the pointers meet without finding a mismatch, I return true. This gives O(n) time and O(1) extra space."

Next TopicIs Subsequence