Valid Palindrome
Compare the string from both ends at once and skip anything that is not a letter or digit. Solved two ways, a cleaned copy in O(n) time and O(n) memory, and two pointers in O(n) time and O(1) memory, with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given a string text. Ignore every character that is not a letter or a digit, and ignore the
difference between uppercase and lowercase. Return true if what is left reads the same forwards
and backwards. Otherwise return false.
Example 1
Input: text = "Top, spot!"
Output: true
Keeping only letters in lowercase gives “topspot”, which reads the same both ways.
Example 2
Input: text = "Was it a cat"
Output: false
The letters are “wasitacat”. Backwards that is “tacatisaw”, which is different.
Example 3
Input: text = ".,!"
Output: true
No letters or digits are left. An empty string reads the same both ways.
Constraints
Section titled “Constraints”- 1 ≤ n ≤ 200,000, where n is the length of
text textholds printable ASCII characters only.
Clarifying questions
Section titled “Clarifying questions”- Do digits count?
- Yes. Letters and digits both count. So "0P" is not a palindrome.
- Does case matter?
- No. "T" and "t" are treated as the same letter.
- What if nothing is left after cleaning?
- Then the answer is true. An empty string is a palindrome.
- Can we build a new string?
- Yes, but it costs O(n) memory. The two-pointer approach avoids it.
Spot the pattern
Section titled “Spot the pattern”A palindrome compares the first character with the last, the second with the second to last, and so on. That is a pair of positions moving toward each other from both ends: the two-pointer pattern. Skipping the extra characters fits in the same loop.
Solutions
Section titled “Solutions”This problem has two approaches. Both are O(n) time; the second one saves the copy.
Clean copy O(n)
The idea
Section titled “The idea”Build a clean copy of the string: walk it once and keep only the letters and digits, each in
lowercase. Then check the copy against its mirror. Compare position i with position clean_length − 1 − i
for the first half of the copy. If any pair differs, it is not a palindrome.
It is simple and fast. The only cost is the extra copy.
Walkthrough
Section titled “Walkthrough”- clean = []
- for character in text:
- if character.isalnum():
- clean.append(character.lower())
- clean_length = len(clean)
- for i in range(clean_length // 2):
- if clean[i] != clean[clean_length - 1 - i]:
- return False
- return True
First build a clean copy: keep only letters and digits, all in lowercase.
class Solution { // Clean copy: keep letters and digits in lowercase, then compare with the mirror. // Two passes, O(n) time. The copy takes O(n) memory. public boolean isPalindrome(String text) { StringBuilder clean = new StringBuilder(); for (char character : text.toCharArray()) { if (Character.isLetterOrDigit(character)) { clean.append(Character.toLowerCase(character)); } } int cleanLength = clean.length(); // Each pair is (i, its mirror). Half the pairs cover them all. for (int i = 0; i < cleanLength / 2; i++) { if (clean.charAt(i) != clean.charAt(cleanLength - 1 - i)) return false; } return true; }}# Clean copy: keep letters and digits in lowercase, then compare with the mirror.# Two passes, O(n) time. The copy takes O(n) memory.def is_palindrome(text: str) -> bool: clean = [] for character in text: if character.isalnum(): clean.append(character.lower()) clean_length = len(clean) # Each pair is (i, its mirror). Half the pairs cover them all. for i in range(clean_length // 2): if clean[i] != clean[clean_length - 1 - i]: return False return Truepublic class Solution{ // Clean copy: keep letters and digits in lowercase, then compare with the mirror. // Two passes, O(n) time. The copy takes O(n) memory. public bool IsPalindrome(string text) { var clean = new System.Text.StringBuilder(); foreach (char character in text) { if (char.IsLetterOrDigit(character)) clean.Append(char.ToLower(character)); } int cleanLength = clean.Length; // Each pair is (i, its mirror). Half the pairs cover them all. for (int i = 0; i < cleanLength / 2; i++) { if (clean[i] != clean[cleanLength - 1 - i]) return false; } return true; }}// Clean copy: keep letters and digits in lowercase, then compare with the mirror.// Two passes, O(n) time. The copy takes O(n) memory.func isPalindrome(text string) bool { clean := []rune{} for _, character := range text { if unicode.IsLetter(character) || unicode.IsDigit(character) { clean = append(clean, unicode.ToLower(character)) } } cleanLength := len(clean) // Each pair is (i, its mirror). Half the pairs cover them all. for i := 0; i < cleanLength/2; i++ { if clean[i] != clean[cleanLength-1-i] { return false } } return true}// Clean copy: keep letters and digits in lowercase, then compare with the mirror.// Two passes, O(n) time. The copy takes O(n) memory.function isPalindrome(text: string): boolean { const clean: string[] = []; for (const character of text) { if (/[a-z0-9]/i.test(character)) clean.push(character.toLowerCase()); } const cleanLength = clean.length; // Each pair is (i, its mirror). Half the pairs cover them all. for (let i = 0; i < Math.floor(cleanLength / 2); i++) { if (clean[i] !== clean[cleanLength - 1 - i]) return false; } return true;}1 Find the work that repeats
There are two loops, one after the other. The first checks one character and maybe adds it to the copy. The second compares one pair. Each run is 1 step.
- clean = []
- for character in text:
- if character.isalnum():
- clean.append(character.lower())
- clean_length = len(clean)
- for i in range(clean_length // 2):
- if clean[i] != clean[clean_length - 1 - i]:
- return False
- return True
2 Count the runs on Example 1
Example 1 has 10 characters, so the cleaning loop runs 10 times. The copy “topspot” has 7 letters, so the compare loop runs 7 // 2 = 3 times.
10 + 3 = 13 runs.
loop clean compare runs on Example 1 10 (one per character) 3 (half of 7) 3 Write the count for any n
The copy has at most n characters, so the compare loop runs at most n / 2 times:
n + n / 2 = 1.5nCheck it with Example 1: the copy is shorter than n, so 13 is below 1.5 × 10 = 15.
4 Keep only the part that grows fastest
Drop the 1.5. A fixed factor does not change how the work grows. What is left is n.
So the time is O(n).
S Space: count the extra memory the same way
The copy
cleancan hold up to n characters, when every character is a letter or digit. So the space is O(n).clean (up to n characters)characteri
Two pointers O(n), O(1) space
The idea
Section titled “The idea”Skip the copy. Put a pointer left at the start and a pointer right at the end. On each step:
- If
leftpoints at something that is not a letter or digit, moveleftone step right. - Else, if
rightpoints at such a character, moverightone step left. - Else compare the two letters in lowercase. If they differ, return false. If they match, move both pointers inward.
Stop when the pointers meet. A middle letter with no partner needs no check.
Why it is correct: the pointers visit exactly the pairs the clean copy would compare, in the same order, just without building the copy.
Walkthrough
Section titled “Walkthrough”- left, right = 0, len(text) - 1
- while left < right:
- if not text[left].isalnum():
- left += 1
- elif not text[right].isalnum():
- right -= 1
- elif text[left].lower() != text[right].lower():
- return False
- else:
- left += 1; right -= 1
- return True
Put one pointer at each end. Compare from the outside in, and skip anything that is not a letter or digit.
class Solution { // Two pointers from both ends, skipping anything that is not a letter or digit. // Each step moves a pointer inward, so O(n) time. No copy, so O(1) memory. public boolean isPalindrome(String text) { int left = 0, right = text.length() - 1; while (left < right) { // Skip one character at a time, so the loop never jumps past the other pointer. if (!Character.isLetterOrDigit(text.charAt(left))) left++; else if (!Character.isLetterOrDigit(text.charAt(right))) right--; else if (Character.toLowerCase(text.charAt(left)) != Character.toLowerCase(text.charAt(right))) return false; else { left++; right--; } } // The pointers met or crossed with every pair matched. return true; }}# Two pointers from both ends, skipping anything that is not a letter or digit.# Each step moves a pointer inward, so O(n) time. No copy, so O(1) memory.def is_palindrome(text: str) -> bool: left, right = 0, len(text) - 1 while left < right: # Skip one character at a time, so the loop never jumps past the other pointer. if not text[left].isalnum(): left += 1 elif not text[right].isalnum(): right -= 1 elif text[left].lower() != text[right].lower(): return False else: left += 1 right -= 1 # The pointers met or crossed with every pair matched. return Truepublic class Solution{ // Two pointers from both ends, skipping anything that is not a letter or digit. // Each step moves a pointer inward, so O(n) time. No copy, so O(1) memory. public bool IsPalindrome(string text) { int left = 0, right = text.Length - 1; while (left < right) { // Skip one character at a time, so the loop never jumps past the other pointer. if (!char.IsLetterOrDigit(text[left])) left++; else if (!char.IsLetterOrDigit(text[right])) right--; else if (char.ToLower(text[left]) != char.ToLower(text[right])) return false; else { left++; right--; } } // The pointers met or crossed with every pair matched. return true; }}// Two pointers from both ends, skipping anything that is not a letter or digit.// Each step moves a pointer inward, so O(n) time. No copy, so O(1) memory.func isPalindrome(text string) bool { isLetterOrDigit := func(character byte) bool { return unicode.IsLetter(rune(character)) || unicode.IsDigit(rune(character)) } left, right := 0, len(text)-1 for left < right { // Skip one character at a time, so the loop never jumps past the other pointer. if !isLetterOrDigit(text[left]) { left++ } else if !isLetterOrDigit(text[right]) { right-- } else if unicode.ToLower(rune(text[left])) != unicode.ToLower(rune(text[right])) { return false } else { left++ right-- } } // The pointers met or crossed with every pair matched. return true}// Two pointers from both ends, skipping anything that is not a letter or digit.// Each step moves a pointer inward, so O(n) time. No copy, so O(1) memory.function isPalindrome(text: string): boolean { const isLetterOrDigit = (character: string) => /[a-z0-9]/i.test(character); let left = 0; let right = text.length - 1; while (left < right) { // Skip one character at a time, so the loop never jumps past the other pointer. if (!isLetterOrDigit(text[left])) left++; else if (!isLetterOrDigit(text[right])) right--; else if (text[left].toLowerCase() !== text[right].toLowerCase()) return false; else { left++; right--; } } // The pointers met or crossed with every pair matched. return true;}1 Find the work that repeats
Each run of the loop does one check or one compare, and then moves at least one pointer inward. That is a fixed amount of work, so each run is 1 step.
- left, right = 0, len(text) - 1
- while left < right:
- if not text[left].isalnum():
- left += 1
- elif not text[right].isalnum():
- right -= 1
- elif text[left].lower() != text[right].lower():
- return False
- else:
- left += 1; right -= 1
- return True
2 Count the runs on Example 1
Example 1 has 10 characters, so the gap between the pointers starts at 9. Each run shrinks it by 1 (a skip) or by 2 (a match). The table shows the 6 runs until the gap is 0.
6 runs.
run 1 2 3 4 5 6 what happened skip "!" match T, t match o, o match p, p skip "," skip "␣" gap after 8 6 4 2 1 0 3 Write the count for any n
The gap starts at n − 1 and every run shrinks it by at least 1. So the loop runs at most:
n − 1 timesCheck it with Example 1: 6 runs, below 10 − 1 = 9, because matches move both pointers.
4 Keep only the part that grows fastest
Drop the − 1. One run more or less does not change how the work grows. What is left is n.
So the time is O(n).
S Space: count the extra memory the same way
The code keeps only
leftandright. That is 2 numbers for any n, so the space is O(1).leftright
Tested at the limits
Section titled “Tested at the limits”2,000 random texts of letters, digits, spaces and punctuation. Half are real palindromes with mixed case, and some have no letters or digits at all. The clean copy and the two pointers gave the same answer every time.
| Largest inputs | Result |
|---|---|
| About 187,000 characters (close to the most allowed), a true palindrome with punctuation, so both pointers walk all the way in. Two pointers. | 0.01 s, and under 0.001 MB of extra memory: just the two pointers. |
| About 187,000 characters, same text. Clean copy. | 0.02 s, and about 7 MB of extra memory: the cleaned copy of 150,000 letters and digits. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.