Check whether a string is a palindrome

s == s[::-1] is the one-liner and allocates a reversed copy. The O(1)-space answer walks two pointers inwards, each skipping non-alphanumeric characters, comparing case-folded. It also short-circuits on the first mismatch, which the one-liner cannot.

Overview

The problem

A palindrome reads the same forwards and backwards. The interview version usually adds a twist: ignore case, and ignore anything that is not alphanumeric.

InputPalindrome?
"racecar"Yes
"A man, a plan, a canal: Panama"Yes — after filtering
"race a car"No
""Yes
" "Yes — nothing left after filtering
"0P"No — 0 and P are both alphanumeric and differ

That last row is the trap most solutions fail: comparing character codes rather than checking alphanumeric status makes '0' and 'P' appear related, because they differ by 32 in ASCII — the same gap as upper and lower case.

StringsCoding problemEasy

Step through it

What to watch

  • Commas and spaces are skipped, not stripped into a new string.
  • Both skip loops need the lo < hi guard.
  • A mismatch stops immediately — no need to check the rest.

Say this out loud

"Two pointers from both ends, skip anything that isn't alphanumeric, compare lowercased. O(n) time, O(1) space, and it bails on the first mismatch."

Check whether a string is a palindrome

Check whether a string is a palindrome, ignoring punctuation and case.

Run it

Both approaches on the same inputs, with the comparison counts printed — which is where the two-pointer version's short-circuit shows up. The last section is the delete-one follow-up.

1Python
Output
2Python
Output
3Python
Output

The two solutions

Filter and compare:

4Python
Output

O(n) time, O(n) space. Clear, short, and it allocates two extra sequences.

Two pointers:

5Python
Output

O(n) time, O(1) space, and it short-circuits on the first mismatch rather than processing the whole string.

The lo < hi condition inside the skip loops matters: without it, a string of only punctuation runs the pointer past the end.

 Filter and compareTwo pointers
TimeO(n)O(n)
SpaceO(n)O(1)
Short-circuitsNoYes
Lines210

Give the short version first, then offer the two-pointer version as the O(1)-space improvement. That sequence — correct answer, then optimisation — is what is being looked for.

Why isalnum and not arithmetic

The tempting optimisation is to compare character codes directly, or to lowercase by adding 32. Both are wrong on real input.

'0' is 48 and 'P' is 80, differing by 32 — the same offset as 'a' and 'A'. Any code that lowercases by adding 32 unconditionally will treat those as equal.

Non-ASCII characters break code arithmetic entirely. 'É'.isalnum() is True, and its code point is nowhere near the ASCII letters.

isalnum() and lower() handle both correctly, and they are C-level operations, so there is no performance argument for the arithmetic version.

Note that isalnum() is Unicode-aware: it returns True for digits and letters in any script, which is usually what is wanted and worth confirming.

The one-liner and its cost

s == s[::-1] is correct and reads well. It builds a full reversed copy first, so it is O(n) extra memory, and it always compares the entire string even when the first and last characters already disagree.

Cleaning first — clean = "".join(c.lower() for c in s if c.isalnum()) then comparing — is readable and costs a second full copy on top.

The two-pointer version

One pointer at each end. Advance each past anything that is not alphanumeric, compare the two characters case-folded, then step both inwards. No copy is made and the first mismatch ends it.

The subtlety is the guards: both inner skip loops need lo < hi in their condition, or a string of pure punctuation runs a pointer off the end. That is the bug this question is really probing.

The follow-up

"Now allow one character to be deleted." On a mismatch, try skipping the left character or the right one and check whether either remaining span is a plain palindrome. That is still O(n): you only get one chance to branch, so the two checks do not nest.

Edge cases

CaseResultWhy
""TrueThe loop does not run
"a"Truelo == hi immediately
".,!"TrueEverything is skipped; the pointers meet
"aa"TrueBoth compared, equal
"ab"FalseImmediate mismatch
"0P"FalseBoth alphanumeric, not equal
Mixed caseTrue if equal after lowering
Non-ASCII lettersHandled by isalnum and lower

The punctuation-only case is the one that breaks naive two-pointer implementations, because the skip loops must be bounded by lo < hi rather than by the array length alone.

The variations

Palindrome with at most one deletion. The most common escalation. On a mismatch, try skipping the left character or the right one, and check whether either remaining substring is a palindrome:

6Python
Output

Still O(n): the helper runs at most twice, and each run is bounded by the remaining length.

Longest palindromic substring. A substantially harder problem — expand around each of the 2n−1 centres for O(n²), or Manacher's algorithm for O(n).

Palindrome number. Reverse the digits arithmetically without converting to a string, which is the version asked when string operations are disallowed.

Palindrome linked list. Find the middle with fast and slow pointers, reverse the second half, compare. O(n) time and O(1) space, and it is a good demonstration of composing techniques.

Can these characters be rearranged into a palindrome? Count characters; at most one may have an odd count. A counting problem rather than a two-pointer one.

What it is testing

Do you handle the filtering requirement rather than assuming clean input?

Do you reach O(1) space? The filter-and-reverse version is correct and allocates; two pointers is the expected improvement.

Do you bound the skip loops? This is the one place the implementation genuinely breaks.

Do you avoid character arithmetic? The "0P" case is deliberately included in test suites to catch it.

Do you ask what counts as a character? Case, punctuation, whitespace and non-ASCII all need a decision.

The transferable pattern is the two-pointer scan from both ends with skipping — it appears in merging, partitioning, and any problem where two positions converge under a condition.

Recap in one screen

  • Ignore case and non-alphanumeric characters unless told otherwise — ask first.
  • Filter-and-compare is two lines and O(n) space; two pointers is O(1) space and short-circuits.
  • Bound the skip loops with lo < hi, or punctuation-only input runs past the end.
  • Use isalnum() and lower(), never character arithmetic — '0' and 'P' differ by 32.
  • The standard escalations are "one deletion allowed" and "longest palindromic substring".

How the code works

Both approaches on the same inputs, with the comparison counts printed — which is where the two-pointer version's short-circuit shows up. The last section is the delete-one follow-up.

How the code works

  1. while lo < hi and not s[lo].isalnum():The guard is the whole trick. Without lo < hi in the inner condition, a string of pure punctuation walks a pointer straight off the end.
  2. s[lo].lower() != s[hi].lower()Case folding happens at the comparison, so no cleaned copy is ever built. casefold() is the more correct choice for non-English text.
  3. return False, comparedThe short-circuit. "race a car" is settled by the first pair; the slice version compares everything regardless.
  4. plain(lo + 1, hi) or plain(lo, hi - 1)The delete-one follow-up. Only one branch is ever taken, so the two checks do not nest and the whole thing stays O(n).

Change one thing

  • Feed it ".,!?". Both return True and the two-pointer version does it without allocating — check the guards are why.
  • Drop lo < hi from one inner loop and run the punctuation-only case. The IndexError is what the guard prevents.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. The main advantage of the two-pointer palindrome check over s == s[::-1] is:

  2. Why do the inner skip loops need `lo < hi` in their condition?

  3. In the 'allow one deletion' variant, why is it still O(n)?

Cheat sheet

Check whether a string is a palindrome

s == s[::-1] is the one-liner and allocates a reversed copy. The O(1)-space answer walks two pointers inwards, each skipping non-alphanumeric characters, comparing case-folded. It also short-circuits on the first mismatch, which the one-liner cannot.

INTERVIEW · vizlearn.in/interview/valid-palindrome.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.