Algorithm

Is Subsequence

Two Pointer Pattern

Is Subsequence

Given two strings s and t, return true if s is a subsequence of t, or false otherwise. A subsequence is formed by deleting some (possibly zero) characters of t without changing the order of the ones that remain — so 'ace' is a subsequence of 'abcde', while 'aec' is not. Return a boolean. An empty s is a subsequence of any t. Follow-up: suppose many s strings s1, s2, ..., sk (k >= 10⁹) all need checking against one fixed t — how would you handle that?

CONSTRAINTS
  • 0 <= s.length <= 100
  • 0 <= t.length <= 10⁴
  • s and t consist only of lowercase English letters
  • Follow-up: many incoming s strings against a single fixed t
EXAMPLE 1
Input: s = "abc", t = "ahbgdc"
Output: true
"a" matches at index 0, "b" matches at index 2, "c" matches at index 5. All characters of s appear in order in t.
EXAMPLE 2
Input: s = "axc", t = "ahbgdc"
Output: false
"a" matches at 0, but "x" does not appear in t after position 0. The scan exhausts t without matching all of s.
EXAMPLE 3
Input: s = "", t = "ahbgdc"
Output: true
An empty string is a subsequence of any string — there are no characters in s to match, so the condition is satisfied trivially.
What is the difference between a subsequence and a substring?
A substring must be contiguous (consecutive characters). A subsequence preserves relative order but allows gaps. 'ace' is a subsequence but not a substring of 'abcde'.
What if s is longer than t?
It is impossible for s to be a subsequence of a shorter string t. The logic handles this naturally — j exhausts t before i exhausts s, and we return false.

We're asked whether the short string s can be found inside the longer string t by deleting some letters of t — keeping the rest in their original order. We may skip as many letters of t as we like, but we can't reorder anything. "ace" hides inside "abcde" (keep a, c, e; drop b, d); "aec" does not, because e comes before c in t. So the real question is about order: do the letters of s appear inside t, left to right, in the same sequence?

The impossible way: try every subsequence

You could generate every subsequence of t and check whether s is among them. But a string of length M has 2ᴹ subsequences — for a t of just 50 characters that's over a quadrillion. Completely unusable, and it also throws away an obvious structural hint: we don't need all subsequences, we need to check one specific target in order.

Two pointers walking two strings, one earning its move

This is the same-direction family once more, and it's close cousin to the read/write walk you saw in Move Zeroes — except now the two pointers walk two different strings. One pointer, i, marks how far into s we've matched so far: it points at the next letter of s we still need to find. The other, j, scans t from left to right and reads every character. j advances on every step no matter what; i advances only when the current letter of t is the one s was waiting for. It's the exact "slow moves only on a keeper" idea — here a "keeper" is a character in t that matches the next needed letter of s. If i walks all the way off the end of s, every letter was found in order, so s is a subsequence.

python
i = 0                              # next letter of s we still need
j = 0                              # scan position in t
while i < len(s) and j < len(t):
    if s[i] == t[j]:               # found the letter s was waiting for
        i += 1                     # advance in s only on a match
    j += 1                         # always advance the scan of t
return i == len(s)                 # matched all of s in order?
Why grabbing the earliest match is always safe

The one thing to justify: when the letter we need appears in t, we take it immediately rather than saving it for later. Why is being greedy correct here? Because matching the earliest possible spot leaves the most of t still ahead of us to match the rest of s. Skipping an early match could only shrink the runway for later letters — it can never help. So the earliest match is never a mistake, which means a single left-to-right pass, taking each letter the first chance it gets, always finds a valid matching if one exists.

Worked Example:s = "abc", t = "ahbgdc"
- i=0 (need 'a'), j=0 reads 'a' → match, i=1 (need 'b'). j=1.
- j=1 reads 'h' → not 'b', keep scanning. j=2.
- j=2 reads 'b' → match, i=2 (need 'c'). j=3.
- j=3 'g', j=4 'd' → neither is 'c', keep scanning. j=5.
- j=5 reads 'c' → match, i=3. i now equals len(s) → loop ends.
- Return i == len(s) → true. Every letter of "abc" was found in order.
The follow-up, and why it changes the tool

The follow-up asks: what if you must test billions of different s strings against the same t? The scan above is O(len(t)) per query — re-reading all of t billions of times is too slow. When one input is fixed and reused, the move is to preprocess it once: build, for each of the 26 letters, a sorted list of the positions where that letter occurs in t. To test an s, track your current position in t and, for each letter of s, binary-search its position list for the first occurrence after where you are. Each query drops to O(len(s) · log len(t)), and t is scanned only once ever. The lesson generalizes past this problem: when the same data answers many queries, spend up front to make each query cheap.

The takeaway for the family: same-direction two pointers don't have to live on one array — here they crawl two strings at once, and the "slow advances only on a match" rule is exactly the write-pointer instinct from the array problems, reused. Whenever you're checking an ordering relationship between two sequences in a single forward pass, this is the shape to reach for.

Interactive Strategy Visualization

Greedy Subsequence Check

Same-Direction Two Pointers

s (subsequence)
i=0
a
b
c
↕ MATCH?
t (target)
j=0
a
0
h
1
b
2
g
3
d
4
c
5
MATCHED
0/3
SCANNED
1/6
STATUS
⏳ Scanning...
Compare s[0]='a' with t[0]='a'. Match!
O(2ᴹ) Generate Subsequences
O(N + M) Time · O(1) Space Two-Pointer Scan