DSA sheet · Strings · Two Pointers pattern
Valid Palindrome
This video opens the strings part of the sheet. The teacher reminds us that string questions mostly fall into just two patterns: two pointers and sliding window. The first two-pointer question on the sheet, Reverse a String, she skips because it is very easy, so she starts directly with Valid Palindrome. It matters because it is the cleanest example of "compare one character from the front with one from the back, then move both inwards". Valid Palindrome II, Longest Palindromic Substring and many others are built on this same move.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Two pointers from scratch (what you must know first)
- Part A · Brute force: clean, reverse and compare
- Part B · Optimal: two pointers from both ends
- Part C · Writing your own "is alphanumeric" check
- Part D · Revision page
Part 0 · Before starting: two pointers from scratch
What is a "pointer" here?
In Python we don't have real memory pointers. A pointer in these notes is simply an index, a whole number that says "I am looking at position i of the array or string". "Two pointers" means we keep two such indexes and move them by our own rules, instead of using two nested loops.
The brute force that two pointers replaces
Many array/string questions talk about pairs of positions (i, j). The simplest idea is to try every pair with two loops: about n × n / 2 pairs, so O(n²). For n = 10⁵ that is about 10¹⁰ steps, far past the rough limit of 10⁸ operations per second, so we get TLE (Time Limit Exceeded).
Two pointers works when something about the input lets us throw away many pairs without checking them. That "something" can be:
- sorted order: if the sum is too big, moving the right pointer left is the only way to make it smaller, so all pairs with the old right end can be skipped;
- the shape of the question: for a palindrome, position i only ever needs to be compared with its mirror position n − 1 − i. All other pairs are useless, so we never look at them.
The kinds of two pointers you'll meet in this notebook
| kind | how the pointers move | example problems |
|---|---|---|
| Opposite ends | left starts at index 0, right at the last index. They walk towards each other and stop when they meet or cross. | Valid Palindrome (this page), Two Sum on a sorted array, Container With Most Water |
| Same direction (slow/fast, read/write) | Both start at the left. The fast one reads every element; the slow one marks where the next "kept" element should be written. | Move Zeroes, Remove Duplicates |
| Three pointers | low, mid, high split the array into 4 zones (0s, 1s, unknown, 2s). | Sort Colors (Dutch National Flag) |
| Fix one + two pointers | A loop fixes element i, then opposite-end pointers search the rest. Skip equal neighbours to avoid duplicate answers. | 3Sum |
| Left-max / right-max | Opposite ends, but each side also remembers the tallest bar it has seen so far. | Trapping Rain Water |
| Palindrome checks | (a) compare from both ends inwards; (b) expand around a center: start in the middle and grow outwards, for odd centers (one character) and even centers (the gap between two characters). | Valid Palindrome, Valid Palindrome II, Longest Palindromic Substring, Palindromic Substrings |
Opposite-end pointers: which one moves, and why nothing is skipped
The rule of opposite-end pointers is: at each step, look at the two values, decide, then move one or both pointers inwards. The safety argument is always the same: the pair we give up on can never be part of the answer. For palindromes this is easy: once s[left] and s[right] have been compared, that pair is finished. No other pair uses these two mirror positions together, so we move both inwards and lose nothing.
Two pointers vs sliding window (the teacher's way to choose)
- Two pointers: we care about exactly two positions at a time, and nothing in between matters for that step. Here: "is the m at the front equal to the m at the back?" The characters between them don't matter yet.
- Sliding window: we care about everything between the two ends, a continuous block (a subarray or substring), like "the sum of this window".
A palindrome check compares two single points, so it is a two-pointer question.
Words used on this page
- Palindrome: a string that reads the same from left to right and from right to left, e.g.
"apa","racecar". - Alphanumeric: a letter (a–z, A–Z) or a digit (0–9). "Alpha" = alphabet, "numeric" = number. Everything else (space, comma, colon, #, ?, _ …) is non-alphanumeric.
- Mirror position: for index i in a string of length n, the mirror is n − 1 − i (first ↔ last, second ↔ second last, …).
Part A · Brute force: clean, reverse and compare
LeetCode 125
1The question in simple words
You get a string s. Return True if it is a palindrome after two clean-up rules, otherwise False:
- Case doesn't matter: capital
Aand smallacount as the same letter. - Ignore everything that is not a letter or digit: spaces, commas, colons and other symbols are simply skipped.
The teacher walks through the Panama example by eye first: the capital A at the front and the small a at the end count as the same, so they match. Then there's a space, which we just step over. Then m and m match, a and a, n and n. Then there's a comma, so we step past it, then a space, so we step past that too, and only then compare the next a with its partner. We only compare when both sides are on a letter or digit.
2What the constraints tell us
1 ≤ s.length ≤ 2 × 10⁵→ at least one character, and n is large-ish. O(n) is clearly fine. O(n²) would be about 4 × 10¹⁰ steps, too slow.shas only printable ASCII characters → letters, digits, spaces and normal symbols. No strange Unicode, sostr.isalnum()andstr.lower()behave as expected.- The teacher's reason for reading constraints: to know whether we need to optimise at all, and if yes, which optimisation will work.
3Intuition
The definition of a palindrome is "same forwards and backwards". So the most direct test is: write the string backwards and see if you get the same thing.
"aman"backwards is"nama". Different → not a palindrome."apa"backwards is"apa". a = a, p = p, a = a → palindrome.
4Building the logic
The teacher's brute force: take a second string s1, put the reverse of s in it, then compare the two strings one position at a time. If any position differs → False. If all positions agree → True.
"A man, a plan, a canal: Panama" as it is, I get "amanaP :lanac a ,nalp a ,nam A". The spaces and commas sit in different places, and A ≠ a. Won't the comparison fail?→ Yes. The plain reverse idea only works after cleaning. So step 1 of our brute force is: keep only letters and digits, and make them all lowercase. Then reverse and compare. (In the video she shows the reverse idea on small clean words like "aman" and "apa"; the cleaning step is what makes it correct for this problem.)
5Approach steps
- Build
clean: go throughs, keep a character only if it is alphanumeric, and store it in lowercase. - Build
rev=cleanreversed (this is the extra copy, the teacher'ss1). - For every index i, if
clean[i] != rev[i]→ return False. - If the loop finishes → return True.
6Code (Python)
class Solution:
def isPalindrome(self, s: str) -> bool:
clean = [ch.lower() for ch in s if ch.isalnum()] # letters/digits only, lowercase
rev = clean[::-1] # the reversed copy (s1)
for i in range(len(clean)):
if clean[i] != rev[i]:
return False
return True7Code line by line
| line | what it means |
|---|---|
| clean = [ch.lower() for ch in s if ch.isalnum()] | Walk once over s. Throw away spaces and symbols, turn letters into lowercase. This is a new list of up to n characters. |
| rev = clean[::-1] | A second new list: the same characters in reverse order. |
| for i in range(len(clean)): | Compare position by position. |
| if clean[i] != rev[i]: return False | Front and back disagree somewhere → not a palindrome. |
| return True | No disagreement anywhere → palindrome. If clean is empty (e.g. s = ", ."), the loop never runs and we return True: an empty string reads the same both ways. |
8Dry run (hand table)
s = "race a car" → clean = r a c e a c a r, rev = r a c a e c a r.
| i | clean[i] | rev[i] | decision |
|---|---|---|---|
| 0 | r | r | same, continue |
| 1 | a | a | same, continue |
| 2 | c | c | same, continue |
| 3 | e | a | different → return False |
For the Panama string, clean = rev = amanaplanacanalpanama, all 21 positions agree, so the answer is True.
9Complexity & remember
- Time O(n): the teacher counts it as about 2n, n to copy/reverse into
s1and n more to compare. 2n is still linear. - Space O(n): the extra copies.
- Will it pass? Yes. With n = 2 × 10⁵, 2n is tiny. Her point: if n were as large as 10⁹, even this would be too slow and too big. And in an interview, two follow-ups are common: "can you make it faster?" and "you're not allowed extra space". Both push us to two pointers.
Part B · Optimal: two pointers from both ends
1The question again, with the new goal
Same question. New goal: no extra string (O(1) extra space) and only one pass over the characters.
2Choosing the pattern
When we want to optimise a string problem, we pick from the two string patterns. At each step we need to compare two single points (front character, back character) and nothing in between. That's two pointers, not sliding window (see Part 0).
3Intuition: two fingers walking towards each other
Put your left finger on the first character and your right finger on the last. Instead of making a reversed copy, read the "backwards" string directly with the right finger.
- If a finger is on a space or a symbol, step over it (left finger forward, right finger backward).
- When both fingers are on letters/digits, compare them ignoring case.
- Different → not a palindrome, stop right away.
- Same → this pair is done. Move both fingers one step inwards and repeat.
- When the fingers meet or cross, every pair has matched → palindrome.
4Building the conditions, the way the teacher does
Condition 1: one match doesn't mean "palindrome"
Suppose s[left] and s[right] are equal. Can we return True? No. A palindrome needs every mirror pair to match, not just one. So on a match we only move the pointers and keep checking.
But one mismatch is enough to say False. For example, if one side shows p and the other shows a, it's over. So the teacher's tip is: look for the False case inside the loop; return True only after the loop, once no mismatch was found.
Condition 2: when should the loop stop? left < right
The pointers move towards the middle. At some point they cross (left goes past right). After that we'd only be comparing pairs we have already checked, so we must stop.
while left < right and not left <= right?→ When
left == right, both fingers are on the same character (the middle of an odd-length string). A character always equals itself, so comparing it is wasted work. <= would still give the right answer, it just does one useless check. So < is enough.Condition 3: skip anything that isn't a letter or digit
Before comparing, each pointer must be on an alphanumeric character.
- If
s[left]is not alphanumeric →left += 1and go round the loop again. - Else, if
s[right]is not alphanumeric →right -= 1and go round again. - Else (both are alphanumeric) → now we may compare.
Python has the built-in str.isalnum() for this (Java has Character.isLetterOrDigit, C++ has isalnum). If you forget it in an interview, you can write your own (Part C).
if / elif / else, only one pointer moves per round. Isn't that slow when there are many spaces in a row?→ No. Every round moves at least one pointer by one step, and the pointers together can only travel n steps before they cross. So the loop runs at most about n times in total. Skipping one symbol per round costs no more than skipping them all with an inner loop.
left pass right and make us compare the wrong pair?→ No. After every skip we go back to the
while left < right check first. If the skip made them meet or cross, the loop simply ends with True. Example: s = ".," → left is on ".", skip → left = 1 = right → loop ends → True (nothing left to compare means it's a palindrome).Condition 4: compare in the same case
The first comparison in the Panama string is 'A' vs 'a'. As raw characters they're different, but the question says they are the same. We don't know which side might be uppercase, so we convert both to lowercase: s[left].lower() != s[right].lower() → return False.
Condition 5: on a match, move both
If they're equal, left += 1 (forward) and right -= 1 (backward), and the loop checks the next pair.
5Approach steps
left = 0,right = len(s) - 1.- While
left < right: - if
s[left]is not alphanumeric → move left forward; - else if
s[right]is not alphanumeric → move right backward; - else if their lowercase forms differ → return False;
- else → move both inwards.
- After the loop → return True.
6Code (Python)
class Solution:
def isPalindrome(self, s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
if not s[left].isalnum(): # left is on a space/symbol
left += 1
elif not s[right].isalnum(): # right is on a space/symbol
right -= 1
elif s[left].lower() != s[right].lower():
return False # a real mismatch
else: # matched pair
left += 1
right -= 1
return True7Code line by line
| line | what it means |
|---|---|
| left, right = 0, len(s) - 1 | Left finger on the first index, right finger on the last index. |
| while left < right: | Keep going while there is still a pair of different positions to compare. Stops when they meet or cross. |
| if not s[left].isalnum(): left += 1 | Left is on something we must ignore. Step forward and re-check from the top. |
| elif not s[right].isalnum(): right -= 1 | We reach this only if the left side is fine. The right side must be ignored, so step backward. |
| elif s[left].lower() != s[right].lower(): return False | Both are letters/digits (the first two checks passed). Compare ignoring case. Different → definitely not a palindrome. |
| else: left += 1 right -= 1 | This mirror pair matched. It's finished, so move both inwards. |
| return True | The loop ended without ever finding a mismatch → every pair matched. |
8Dry run
s = "A man, a plan, a canal: Panama" (30 characters, index 0 to 29). ␣ = a space.
Yellow = where the pointers are now. Grey = already finished (matched or skipped).
Every round, step by step:
| step | left | right | s[left] | s[right] | decision | after |
|---|---|---|---|---|---|---|
| 1 | 0 | 29 | A | a | a = a → move both | L=1, R=28 |
| 2 | 1 | 28 | ␣ | m | left not alnum → left + 1 | L=2, R=28 |
| 3 | 2 | 28 | m | m | same → move both | L=3, R=27 |
| 4 | 3 | 27 | a | a | same → move both | L=4, R=26 |
| 5 | 4 | 26 | n | n | same → move both | L=5, R=25 |
| 6 | 5 | 25 | , | a | left not alnum → left + 1 | L=6, R=25 |
| 7 | 6 | 25 | ␣ | a | left not alnum → left + 1 | L=7, R=25 |
| 8 | 7 | 25 | a | a | same → move both | L=8, R=24 |
| 9 | 8 | 24 | ␣ | P | left not alnum → left + 1 | L=9, R=24 |
| 10 | 9 | 24 | p | P | p = p → move both | L=10, R=23 |
| 11 | 10 | 23 | l | ␣ | right not alnum → right − 1 | L=10, R=22 |
| 12 | 10 | 22 | l | : | right not alnum → right − 1 | L=10, R=21 |
| 13 | 10 | 21 | l | l | same → move both | L=11, R=20 |
| 14 | 11 | 20 | a | a | same → move both | L=12, R=19 |
| 15 | 12 | 19 | n | n | same → move both | L=13, R=18 |
| 16 | 13 | 18 | , | a | left not alnum → left + 1 | L=14, R=18 |
| 17 | 14 | 18 | ␣ | a | left not alnum → left + 1 | L=15, R=18 |
| 18 | 15 | 18 | a | a | same → move both | L=16, R=17 |
| 19 | 16 | 17 | ␣ | c | left not alnum → left + 1 | L=17, R=17 |
| – | 17 | 17 | c | c | left < right is false → loop ends | return True |
Notice the middle c is never compared. It's its own mirror, which is exactly the < vs <= point.
A False example: "race a car"
| step | left | right | s[left] | s[right] | decision | after |
|---|---|---|---|---|---|---|
| 1 | 0 | 9 | r | r | same → move both | L=1, R=8 |
| 2 | 1 | 8 | a | a | same → move both | L=2, R=7 |
| 3 | 2 | 7 | c | c | same → move both | L=3, R=6 |
| 4 | 3 | 6 | e | ␣ | right not alnum → right − 1 | L=3, R=5 |
| 5 | 3 | 5 | e | a | different → stop | return False |
9Complexity & remember
- Time O(n): the two pointers start at the two ends and meet around the middle. The teacher calls it about n/2 comparisons, which is O(n). Skipped symbols add at most one round each, so it's still linear. Nothing is copied.
- Space O(1): just two integers. This is what makes it better than Part A.
- On LeetCode this ran as the fastest solution in her submission.
left = 0, right = n−1, while left < right: skip non-alnum on the left · else skip non-alnum on the right · else compare lowercase: differ → False, same → move both. After the loop → True.Part C · Writing your own "is alphanumeric" check
The teacher's interview tip: every language has a built-in for this (isalnum in Python and C++, isLetterOrDigit in Java). If you forget its name in an interview, don't panic. Write a tiny helper yourself.
1The idea
Every character has a number code (its ASCII value, which Python gives with ord(ch)). Letters and digits sit in three continuous ranges:
| range | characters | ASCII codes |
|---|---|---|
| uppercase | 'A' … 'Z' | 65 … 90 |
| lowercase | 'a' … 'z' | 97 … 122 |
| digits | '0' … '9' | 48 … 57 |
So a character is alphanumeric if it falls inside any of the three ranges. In Python you can compare characters directly ('a' <= ch <= 'z'), which compares their codes for you.
2Code (Python)
class Solution:
def isPalindrome(self, s: str) -> bool:
def is_alnum(ch):
return ('a' <= ch <= 'z') or ('A' <= ch <= 'Z') or ('0' <= ch <= '9')
left, right = 0, len(s) - 1
while left < right:
if not is_alnum(s[left]):
left += 1
elif not is_alnum(s[right]):
right -= 1
elif s[left].lower() != s[right].lower():
return False
else:
left += 1
right -= 1
return True3Line by line, complexity
| line | what it means |
|---|---|
| ('a' <= ch <= 'z') | Is it a small letter? |
| ('A' <= ch <= 'Z') | Or a capital letter? |
| ('0' <= ch <= '9') | Or a digit? If any one is True, the character counts. |
| rest of the code | Exactly Part B, with is_alnum(x) instead of x.isalnum(). |
The helper does at most 6 comparisons, so it costs O(1) per call. The whole solution stays O(n) time, O(1) space.
isalnum()?→ For this problem, yes, because the input is plain ASCII. In general Python's
isalnum() also accepts non-English letters and digits (like "é" or "٣"), while the helper only accepts a–z, A–Z, 0–9.Part D · Revision page
| Brute force (Part A) | Two pointers (Part B / C) | |
|---|---|---|
| idea | clean → reverse into a copy → compare | read the string from both ends at once |
| symbols / spaces | removed while building the copy | skipped by moving the pointer that's on one |
| case | lower() while copying | lower() on both sides before comparing |
| stop | after comparing all positions | while left < right (meet or cross) |
| time | O(n) (about 2n) | O(n) (about n/2 comparisons) |
| extra space | O(n) | O(1) |
2. Two pointers (not sliding window): only two points matter at a time.
3. Skip non-alphanumeric on either side before comparing.
4. Compare
lower() of both. One mismatch → False; True only after the loop.5.
while left < right; the middle character never needs checking.✗ comparing without
lower() (A ≠ a)✗ comparing a space or comma instead of skipping it
✗ reversing the raw string without cleaning it first
✗ moving the pointers past each other and comparing again (use
left < right)s = Solution()
print(s.isPalindrome("A man, a plan, a canal: Panama")) # True
print(s.isPalindrome("race a car")) # False
print(s.isPalindrome(" ")) # True (nothing to compare)
print(s.isPalindrome(".,:")) # True
print(s.isPalindrome("0P")) # False (digit 0 vs letter p)
print(s.isPalindrome("aA")) # TrueBased on this video: Valid Palindrome | Two Pointers