Skip to content

Coding patternsBlind 75

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.

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.

text
T0o1p2,3␣4s5p6o7t8!9

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.

  • 1 ≤ n ≤ 200,000, where n is the length of text
  • text holds printable ASCII characters only.
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.

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.

This problem has two approaches. Both are O(n) time; the second one saves the copy.

Two pointers O(n), O(1) space

Skip the copy. Put a pointer left at the start and a pointer right at the end. On each step:

  • If left points at something that is not a letter or digit, move left one step right.
  • Else, if right points at such a character, move right one 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.

text
T0o1p2,3␣4s5p6o7t8!9
leftright
  1. left, right = 0, len(text) - 1
  2. while left < right:
  3. if not text[left].isalnum():
  4. left += 1
  5. elif not text[right].isalnum():
  6. right -= 1
  7. elif text[left].lower() != text[right].lower():
  8. return False
  9. else:
  10. left += 1; right -= 1
  11. return True

Put one pointer at each end. Compare from the outside in, and skip anything that is not a letter or digit.

1 / 8
Solution.javasolution.pySolution.cssolution.gosolution.ts
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;
}
}
Time O(n)Space O(1)Here is how we get these from the code, in four steps.
  1. 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.

    1. left, right = 0, len(text) - 1
    2. while left < right:
    3. if not text[left].isalnum():
    4. left += 1
    5. elif not text[right].isalnum():
    6. right -= 1
    7. elif text[left].lower() != text[right].lower():
    8. return False
    9. else:
    10. left += 1; right -= 1
    11. return True
  2. 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.

    run123456
    what happenedskip "!"match T, tmatch o, omatch p, pskip ","skip "␣"
    gap after864210
  3. 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 times

    Check it with Example 1: 6 runs, below 10 − 1 = 9, because matches move both pointers.

  4. 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).

  5. S Space: count the extra memory the same way

    The code keeps only left and right. That is 2 numbers for any n, so the space is O(1).

    leftright

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 inputsResult
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.