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?
- 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
s = "abc", t = "ahbgdc"trues = "axc", t = "ahbgdc"falses = "", t = "ahbgdc"trueWe'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?
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.
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.
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?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.
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.
Greedy Subsequence Check
Same-Direction Two Pointers
Key Insight
Pointer i only advances on a match, while j always advances. This greedy strategy works because matching a character earlier in t never hurts — it leaves more room for future matches.
Asymmetric Pointers
Unlike converging pointers that move toward each other, these two pointers move in the same direction at different speeds. One is a "scanner" (always advancing), the other is a "matcher" (only on match).