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 · 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:

The kinds of two pointers you'll meet in this notebook

kindhow the pointers moveexample problems
Opposite endsleft 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 pointerslow, mid, high split the array into 4 zones (0s, 1s, unknown, 2s).Sort Colors (Dutch National Flag)
Fix one + two pointersA loop fixes element i, then opposite-end pointers search the rest. Skip equal neighbours to avoid duplicate answers.3Sum
Left-max / right-maxOpposite 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)

A palindrome check compares two single points, so it is a two-pointer question.

Words used on this page

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:

inputA man, a plan, a canal: Panama
cleanedamanaplanacanalpanamareads the same both ways → True
inputrace a car
cleanedraceacarbackwards it is "racaecar" → False

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

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.

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.

Doubt: if I reverse "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

  1. Build clean: go through s, keep a character only if it is alphanumeric, and store it in lowercase.
  2. Build rev = clean reversed (this is the extra copy, the teacher's s1).
  3. For every index i, if clean[i] != rev[i] → return False.
  4. If the loop finishes → return True.

6Code (Python)

Valid Palindrome, brute force (reverse copy)
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 True

7Code line by line

linewhat 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 FalseFront and back disagree somewhere → not a palindrome.
return TrueNo 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.

iclean[i]rev[i]decision
0rrsame, continue
1aasame, continue
2ccsame, continue
3eadifferent → return False

For the Panama string, clean = rev = amanaplanacanalpanama, all 21 positions agree, so the answer is True.

9Complexity & remember

Remember the brute forceClean (letters/digits, lowercase) → reverse into a copy → compare. Correct, O(n) time, but O(n) extra space.

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.

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.

Doubt: why 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.

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

Doubt: with 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.
Doubt: after skipping, could 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

  1. left = 0, right = len(s) - 1.
  2. While left < right:
  3.   if s[left] is not alphanumeric → move left forward;
  4.   else if s[right] is not alphanumeric → move right backward;
  5.   else if their lowercase forms differ → return False;
  6.   else → move both inwards.
  7. After the loop → return True.

6Code (Python)

Valid Palindrome, two pointers (O(1) extra space)
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 True

7Code line by line

linewhat it means
left, right = 0, len(s) - 1Left 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 += 1Left is on something we must ignore. Step forward and re-check from the top.
elif not s[right].isalnum(): right -= 1We 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 FalseBoth are letters/digits (the first two checks passed). Compare ignoring case. Different → definitely not a palindrome.
else: left += 1 right -= 1This mirror pair matched. It's finished, so move both inwards.
return TrueThe 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.

index01234567891011121314151617181920212223242526272829
startA␣man,␣a␣plan,␣a␣canal:␣PanamaA vs a → same after lowercase
L                            R
step 9A␣man,␣a␣plan,␣a␣canal:␣PanamaL on a space → skip it
        L               R     
endA␣man,␣a␣plan,␣a␣canal:␣PanamaL = R = 17 → loop stops → True
                 L/R            

Yellow = where the pointers are now. Grey = already finished (matched or skipped).

Every round, step by step:

stepleftrights[left]s[right]decisionafter
1029Aaa = a → move bothL=1, R=28
2128␣mleft not alnum → left + 1L=2, R=28
3228mmsame → move bothL=3, R=27
4327aasame → move bothL=4, R=26
5426nnsame → move bothL=5, R=25
6525,aleft not alnum → left + 1L=6, R=25
7625␣aleft not alnum → left + 1L=7, R=25
8725aasame → move bothL=8, R=24
9824␣Pleft not alnum → left + 1L=9, R=24
10924pPp = p → move bothL=10, R=23
111023l␣right not alnum → right − 1L=10, R=22
121022l:right not alnum → right − 1L=10, R=21
131021llsame → move bothL=11, R=20
141120aasame → move bothL=12, R=19
151219nnsame → move bothL=13, R=18
161318,aleft not alnum → left + 1L=14, R=18
171418␣aleft not alnum → left + 1L=15, R=18
181518aasame → move bothL=16, R=17
191617␣cleft not alnum → left + 1L=17, R=17
–1717ccleft < right is false → loop endsreturn 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 4race␣a␣carr, a, c already matched. Right is on a space → right − 1
   L  R   
step 5race␣a␣care vs a → mismatch → return False
   L R    
stepleftrights[left]s[right]decisionafter
109rrsame → move bothL=1, R=8
218aasame → move bothL=2, R=7
327ccsame → move bothL=3, R=6
436e␣right not alnum → right − 1L=3, R=5
535eadifferent → stopreturn False

9Complexity & remember

Remember Valid Palindrome 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:

rangecharactersASCII 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)

Valid Palindrome, two pointers with a hand-written check
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 True

3Line by line, complexity

linewhat 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 codeExactly 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.

Doubt: is the hand-written check exactly the same as Python's 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.
RememberForgot the built-in? Check the three ranges a–z, A–Z, 0–9. It's O(1).

Part D · Revision page

Brute force (Part A)Two pointers (Part B / C)
ideaclean → reverse into a copy → compareread the string from both ends at once
symbols / spacesremoved while building the copyskipped by moving the pointer that's on one
caselower() while copyinglower() on both sides before comparing
stopafter comparing all positionswhile left < right (meet or cross)
timeO(n) (about 2n)O(n) (about n/2 comparisons)
extra spaceO(n)O(1)
If you remember only 5 lines 1. Palindrome = same forwards and backwards. Compare mirror pairs.
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.
Mistakes to avoid ✗ returning True on the first match (every pair must match)
✗ 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)
test it yourself (paste under any of the solutions above)
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"))                              # True

Based on this video: Valid Palindrome | Two Pointers