Learn/DSA Patterns
DSA PatternsStringseasy9 min read

Two Pointers on Strings

The same opposite-direction and same-direction two-pointer techniques from arrays apply directly to strings. This chapter covers palindrome checking, subsequence testing, and comparing two strings character by character — with the exact same skeleton as Chapter 1.1 and 1.2, just on characters instead of numbers.

#two-pointers#strings#palindrome#subsequence#beginner#interview
Table of contents

A quick connection

In Chapter 1.1 you used two pointers walking toward each other to find pairs in a sorted array. In Chapter 1.2 you used two pointers walking in the same direction to merge or copy. Those skeletons work unchanged on strings — a string is just an array of characters.

This chapter is deliberately short. The patterns are the same; the only newness is recognising string-flavoured problem statements as two-pointer problems, and handling two small string-specific gotchas (case, non-alpha characters).


Pattern A — Opposite direction: palindrome check

A real-life story

Two readers start at opposite ends of a word and walk toward each other, comparing the letters they land on. If they always find matching letters and meet in the middle without a mismatch, the word reads the same both ways. That is palindrome checking — two pointers, opposite direction.

The skeleton

left, right = 0, len(s) - 1
while left < right:
    if s[left] != s[right]:
        return False          # mismatch found
    left  += 1
    right -= 1
return True

Valid Palindrome (LC #125) — ignore non-alphanumeric, ignore case

def isPalindrome(s: str) -> bool:
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left  += 1        # skip non-alphanumeric from left
        while left < right and not s[right].isalnum():
            right -= 1        # skip non-alphanumeric from right
        if s[left].lower() != s[right].lower():
            return False
        left  += 1
        right -= 1
    return True

Frame-by-frame: "A man, a plan, a canal: Panama"

After stripping non-alpha in-place (conceptually):
"amanaplanacanalpanama"

left=0 'a', right=19 'a'match, advance
left=1 'm', right=18 'm'match, advance
left=2 'a', right=17 'a'match, advance
...
All characters matchTrue

Pause & think

What's the difference between s[left].isalnum() and s[left].isalpha()? When does that matter?

Answer

isalpha() returns True only for letters (a-z, A-Z). isalnum() returns True for letters and digits (0-9). For palindrome problems, digits count as valid characters — "race1car" with digits should compare '1' with '1'. Using isalpha() would incorrectly skip digits. Always use isalnum() for these problems unless the problem explicitly says "letters only."


Pattern B — Same direction: is subsequence

A subsequence of string t is a string s whose characters appear in t in the same order but not necessarily consecutively. "ace" is a subsequence of "abcde"; "aec" is not.

A real-life story

A librarian scans a shelf of books (string t) looking for a reading list (string s). She has one finger on her list and one finger on the shelf. When the shelf book matches the list entry, she advances both fingers. Otherwise she advances only the shelf finger. If she finishes the list, all entries were found in order.

The skeleton

i, j = 0, 0                      # i → s, j → t
while i < len(s) and j < len(t):
    if s[i] == t[j]:
        i += 1                    # matched one more character of s
    j += 1                        # always advance through t
return i == len(s)                # did we consume all of s?

Frame-by-frame: s = "ace", t = "abcde"

i=0 j=0: s[0]='a' == t[0]='a' → match, i=1, j=1
i=1 j=1: s[1]='c' != t[1]='b'no match, j=2
i=1 j=2: s[1]='c' == t[2]='c' → match, i=2, j=3
i=2 j=3: s[2]='e' != t[3]='d'no match, j=4
i=2 j=4: s[2]='e' == t[4]='e' → match, i=3, j=5
i=3 → i == len(s) → True

O(n) time, O(1) space — no extra array, no sorting, single pass.


Pattern C — Two-string comparison: merge/align

Some problems give you two strings and ask you to compare them character by character, skipping or inserting characters. These use two separate pointers, one per string.

Backspace String Compare (LC #844)

Given two strings where # means backspace, check if they produce the same result after processing.

def backspaceCompare(s: str, t: str) -> bool:
    i, j = len(s) - 1, len(t) - 1
    skip_s = skip_t = 0

    while i >= 0 or j >= 0:
        # find the next valid character in s
        while i >= 0:
            if s[i] == '#':
                skip_s += 1
                i -= 1
            elif skip_s > 0:
                skip_s -= 1
                i -= 1
            else:
                break

        # find the next valid character in t
        while j >= 0:
            if t[j] == '#':
                skip_t += 1
                j -= 1
            elif skip_t > 0:
                skip_t -= 1
                j -= 1
            else:
                break

        # compare the two valid characters
        if i >= 0 and j >= 0:
            if s[i] != t[j]:
                return False
        elif i >= 0 or j >= 0:
            return False             # one string has extra characters

        i -= 1
        j -= 1

    return True

Why scan from the right? Because # deletes the character to its left. Scanning right-to-left lets us count pending backspaces and skip the correct number of characters without building a new string.

O(n + m) time, O(1) space — superior to the O(n) space approach of building both result strings and comparing.


The one idea to remember

The entire pattern in one sentence

String two-pointer problems are array two-pointer problems on characters: opposite-direction pointers check symmetry (palindromes), same-direction pointers test order (subsequences), and dual pointers (one per string) align or merge two streams — all in O(n) time and O(1) space.


Where to spot this pattern

Trigger words:

  • "palindrome" or "reads the same forwards and backwards"
  • "subsequence" — is one string hidden in another in order?
  • "after processing / simulating" — backspace, delete, skip
  • "compare two strings" ignoring certain characters
  • "reverse the string" — setup for palindrome or reversal trick (Chapter 3.7)

5 disguises:

  1. Valid Palindrome (LC #125): skip non-alphanumeric; case-insensitive.
  2. Valid Palindrome II (LC #680): allowed to delete at most one character — try skipping left or right on first mismatch.
  3. Is Subsequence (LC #392): same-direction classic.
  4. Backspace String Compare (LC #844): dual right-to-left with skip counters.
  5. Long Pressed Name (LC #925): same-direction, allow repeated characters in typed string.

Common traps

Watch out for these

  • Using isalpha() instead of isalnum() in Valid Palindrome — digits count as valid characters and should not be skipped.
  • left < right vs left <= right in the while condition. Use < (strict) — when left == right you're at the middle of an odd-length palindrome and there's nothing to compare; it's trivially valid.
  • In Is Subsequence: returning True when j exhausts but i hasn't. The check return i == len(s) handles this correctly — only return True if all characters of s were matched.
  • Building a new string for Backspace Compare. Correct but O(n) space. The right-to-left pointer approach solves it in O(1) space — prefer this in interviews.

Complexity

ProblemTimeSpace
Valid PalindromeO(n)O(1)
Is SubsequenceO(n + m)O(1)
Backspace String CompareO(n + m)O(1)

Say it like a pro (interview one-liner)

"This is a two-pointer problem on a string. For palindromes, I walk inward from both ends comparing characters. For subsequences, I use one pointer per string advancing together. All variants run in O(n) time and O(1) space — no extra array needed."


Remember this forever

Two Pointers on Strings — 3 skeletons

Palindrome (opposite direction):

left, right = 0, len(s) - 1
while left < right:
    skip non-alphanumeric, compare s[left].lower() vs s[right].lower()
    if mismatch → False; else advance both
return True

Subsequence (same direction):

i, j = 0, 0
while i < len(s) and j < len(t):
    if s[i] == t[j]: i += 1
    j += 1
return i == len(s)

Dual-string (right-to-left with skip):

  • Count backspaces, skip characters, compare valid chars from right.

Check yourself

For Valid Palindrome II, how do you handle the "delete one character" allowance?

Run the standard palindrome check. On the first mismatch at positions left and right, try two sub-problems: skip left (check s[left+1..right]) or skip right (check s[left..right-1]). If either half is a palindrome, the whole string is a valid palindrome with one deletion. This is still O(n) — you scan at most twice.

Why does "Is Subsequence" advance `j` regardless of match, but only advance `i` on a match?

j scans through the longer string t looking for characters of s. Every position in t must be visited (we might need any character). i only advances when we actually match the current s[i] — advancing it before a match would skip a required character of s.

In Backspace String Compare, what happens if a `#` appears at the very beginning of the string (no character to delete)?

The skip_s counter increments, but when the inner while loop tries to consume a non-# character with a pending skip, it just decrements skip_s and moves i left — skipping that character. If i reaches -1 while skip_s > 0, the while condition i >= 0 terminates the loop. The remaining skip count is simply discarded (no more characters to delete). This is the correct behaviour: leading backspaces in a typed sequence with nothing before them have no effect.


Practice problems

ProblemDifficultyWhat to noticeLink
Valid PalindromeEasyisalnum(), case-fold, left < rightLC #125
Valid Palindrome IIEasyOn mismatch, try skipping left or rightLC #680
Is SubsequenceEasySame-direction; return i == len(s)LC #392
Backspace String CompareEasyRight-to-left with skip counter; O(1) spaceLC #844
Long Pressed NameEasySame-direction; allow repeated chars in typedLC #925

Next up: Sliding Window on Strings — maintaining a character frequency map inside a moving window to find minimum windows, permutation matches, and anagram occurrences.