Solving tips
- Recognize order-preserving matching: a greedy two-pointer scan works because taking the earliest usable occurrence of each needed character is never worse.
- Keep one pointer into s, walk t once, advance the pointer on each match, and answer True iff the pointer reaches len(s); O(|t|) time, O(1) space.
- For the many-queries follow-up, preprocess t into a map letter->sorted positions and binary-search (bisect_right) for the first position strictly after the last match.
- Pitfall: guard s[i] with i < len(s) once s is exhausted, and use bisect_right (not bisect_left) so you don't reuse the same position.
Problem
Given two strings s and t, return True if s is a subsequence of t, and False otherwise.
A subsequence keeps characters in their original relative order but may skip any number of characters. Formally, s is a subsequence of t if you can delete zero or more characters from t (without reordering the rest) and obtain s. The empty string is a subsequence of everything.
Follow-up: if a huge number of query strings s1, s2, ..., sk (say k >= 10^9) will each be tested against the same t, how would you preprocess t to answer each query fast?
Examples
s = "abc", t = "ahbgdc" β True β take a (index 0), b (index 2), c (index 5), in order.
s = "axc", t = "ahbgdc" β False β after matching a, no x appears anywhere later in t.
s = "", t = "xyz" β True β the empty string is a subsequence of any string.
Constraints
0 <= len(s) <= 100
0 <= len(t) <= 10^4
- Both strings consist of lowercase English letters only.
A single O(|t|) scan answers one query; the follow-up wants each query in roughly O(|s| log |t|) after preprocessing.
Think about it first
Hint 1
To match s inside t, does it ever hurt to take the EARLIEST occurrence in t of the character you currently need?
Hint 2
Keep one pointer into s. Walk t once; when the current t character equals the s character under the pointer, advance the pointer. What does the pointer equal at the end if s fits?
Hint 3
For the follow-up: record, for each letter, the sorted list of its positions in t. To match the next character of a query after position p, binary-search that letter's list for the first position greater than p.
TL;DR
Greedy two-pointer scan β O(|t|) time, O(1) space (plus an index-map + binary-search variant for the many-queries follow-up).
Approach 1 β Brute force (recursive matching)
At each pair of positions (i, j) β i into s, j into t β either the characters match and both advance, or tβs character is skipped. Recurse until s is exhausted (success) or t is exhausted (failure).
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
def match(i: int, j: int) -> bool:
if i == len(s):
return True
if j == len(t):
return False
if s[i] == t[j]:
return match(i + 1, j + 1)
return match(i, j + 1)
return match(0, 0)
Complexity: O(|t|) time β each call advances j, so there are at most |t| frames β but also O(|t|) stack depth. That is what kills it: at |t| = 10^4 this brushes against CPythonβs default recursion limit (1000 by default, and even when raised, deep recursion is slow and fragile). The logic is right; the shape is wrong.
Approach 2 β Greedy two pointers
The insight: when looking for the next character of s, taking its earliest occurrence in t is never worse β any later choice only shrinks what remains of t for the rest of s. This exchange argument makes a simple greedy algorithm (commit to each locally-earliest match, never backtrack) correct, and it converts the recursion into a flat scan.
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
i = 0 # next character of s still to match
for ch in t:
if i < len(s) and s[i] == ch:
i += 1
return i == len(s)
Walkthrough on s = "abc", t = "ahbgdc":
| ch from t | s[i] needed | match? | i after |
|---|
| a | a | yes | 1 |
| h | b | no | 1 |
| b | b | yes | 2 |
| g | c | no | 2 |
| d | c | no | 2 |
| c | c | yes | 3 |
i == len(s) == 3 β True. On s = "axc" the pointer sticks at x forever and ends at 1 β False.
Complexity: O(|t|) time, O(1) space.
Approach 3 β Index map + binary search (the follow-up)
The insight: with billions of queries against one fixed t, re-scanning t per query is the waste. Preprocess t once into a map from each letter to the sorted list of its positions; then matching a query character after position p is βfirst occurrence of this letter strictly after pβ β a binary search (O(log) lookup in a sorted list) via bisect_right.
from bisect import bisect_right
from collections import defaultdict
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
positions: defaultdict[str, list[int]] = defaultdict(list)
for idx, ch in enumerate(t):
positions[ch].append(idx)
prev = -1 # position in t of the last matched character
for ch in s:
occ = positions[ch]
k = bisect_right(occ, prev)
if k == len(occ):
return False # no occurrence of ch after prev
prev = occ[k]
return True
Walkthrough on s = "abc", t = "ahbgdc":
- Preprocessing:
a β [0], h β [1], b β [2], g β [3], d β [4], c β [5].
ch = 'a', prev = -1: first position in [0] after -1 is 0 β prev = 0.
ch = 'b', prev = 0: first position in [2] after 0 is 2 β prev = 2.
ch = 'c', prev = 2: first position in [5] after 2 is 5 β prev = 5. All matched β True.
For s = "axc": positions['x'] is empty, bisect_right returns 0 = its length β False.
Complexity: O(|t|) preprocessing once, then O(|s| log |t|) per query, O(|t|) space for the map. For a single query the two-pointer scan wins; for k queries this turns O(k Β· |t|) into O(|t| + k Β· |s| log |t|).
Common pitfalls
- Confusing subsequence with substring β characters need not be contiguous, only in order.
"ace" is a subsequence of "abcde", not a substring.
- Forgetting the empty-
s edge case: it must return True (the loop never advances i, and 0 == len(s) holds β but only if your final check is i == len(s), not i > 0).
- In the follow-up, using
bisect_left(occ, prev) β that can re-use the same position as the previous match; you need strictly-after, i.e. bisect_right.
- Indexing
s[i] without the i < len(s) guard once s is fully matched β the scan of t continues after the last match.
Pattern takeaway
Order-preserving matching against a fixed sequence is greedy-safe: always take the earliest usable occurrence, which reduces the whole problem to one forward scan with a single pointer. And when the same haystack serves many queries, invert it once into a value β sorted-positions hash map so each query walks its own needle with logarithmic lookups β the standard βpreprocess the static sideβ move.