392. Is Subsequence - Explanation
Description
You are given two strings s and t, return true if s is a subsequence of t, or false otherwise.
A subsequence of a string is a new string that is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (i.e., "ace" is a subsequence of "abcde" while "aec" is not).
Example 1:
Input: s = "node", t = "neetcode"
Output: trueExample 2:
Input: s = "axc", t = "ahbgdc"
Output: falseConstraints:
0 <= s.length <= 1000 <= t.length <= 10,000sandtconsist only of lowercase English letters.
Follow up: Suppose there are lots of incoming s, say s1, s2, ..., sk where k >= 1,000,000,000, and you want to check one by one to see if t has its subsequence. In this scenario, how would you change your code?
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Two Pointers - Traversing two sequences simultaneously with independent pointers
- Recursion - Breaking down the problem into smaller subproblems with base cases
- Dynamic Programming - Using memoization or tabulation to avoid redundant computations
1. Recursion
Intuition
To check if s is a subsequence of t, we need to find all characters of s in t in the same order, though not necessarily contiguous. Recursively, we compare the current characters of both strings: if they match, we advance both pointers; if they do not match, we only advance the pointer in t to continue searching. The base cases are reaching the end of s (success) or the end of t before finishing s (failure).
Algorithm
- Define a recursive function
rec(i, j)whereiis the index insandjis the index int. - Base cases:
- If
i == len(s), all characters matched, returntrue. - If
j == len(t), ran out of characters int, returnfalse.
- If
- If
s[i] == t[j], both characters match, so recurse withrec(i + 1, j + 1). - Otherwise, skip the current character in
tand recurse withrec(i, j + 1). - Start with
rec(0, 0).
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the length of the string .
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution may recompute the same subproblems multiple times. By adding memoization, we cache the result for each (i, j) state so that each pair is computed at most once. This transforms the exponential worst case into a polynomial time solution while keeping the recursive structure intact.
Algorithm
- Create a 2D memo table initialized to
-1. - Define a recursive function
rec(i, j):- Base cases same as before.
- If
memo[i][j]is already computed, return the cached result. - Compute the result based on whether characters match or not.
- Store the result in
memo[i][j]and return it.
- Call
rec(0, 0).
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
n, m = len(s), len(t)
memo = [[-1] * m for _ in range(n)]
def rec(i, j):
if i == n:
return True
if j == m:
return False
if memo[i][j] != -1:
return memo[i][j] == 1
if s[i] == t[j]:
memo[i][j] = 1 if rec(i + 1, j + 1) else 0
else:
memo[i][j] = 1 if rec(i, j + 1) else 0
return memo[i][j] == 1
return rec(0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the length of the string .
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursion with memoization, we can fill a DP table iteratively from the end of both strings toward the beginning. The value dp[i][j] represents whether s[i:] is a subsequence of t[j:]. If the characters match, we look at dp[i+1][j+1]. Otherwise, we look at dp[i][j+1] (skip the character in t). The base case is that any suffix of s starting at len(s) is trivially a subsequence of anything (empty string).
Algorithm
- Create a 2D DP table of size
(n+1) x (m+1)initialized tofalse. - Set
dp[n][j] = truefor allj(empty remainder ofsis always a subsequence). - Iterate
ifromn-1down to0, andjfromm-1down to0:- If
s[i] == t[j], setdp[i][j] = dp[i+1][j+1]. - Otherwise, set
dp[i][j] = dp[i][j+1].
- If
- Return
dp[0][0].
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
n, m = len(s), len(t)
dp = [[False] * (m + 1) for _ in range(n + 1)]
for j in range(m + 1):
dp[n][j] = True
for i in range(n - 1, -1, -1):
for j in range(m - 1, -1, -1):
if s[i] == t[j]:
dp[i][j] = dp[i + 1][j + 1]
else:
dp[i][j] = dp[i][j + 1]
return dp[0][0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the length of the string .
4. Two Pointers
Intuition
The most efficient approach uses two pointers since we only need to make a single pass through both strings. Pointer i tracks our position in s, and pointer j tracks our position in t. We always advance j, but only advance i when we find a matching character. If we reach the end of s, all characters were found in order. This is optimal because each character in t is examined exactly once.
Algorithm
- Initialize pointers
i = 0andj = 0. - While
i < len(s)andj < len(t):- If
s[i] == t[j], incrementi. - Always increment
j.
- If
- Return
i == len(s).
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the length of the string .
5. Follow-Up Solution (Index Jumping)
Intuition
When checking many strings against the same t, the two-pointer approach becomes inefficient because we scan t repeatedly. Instead, we precompute for each position in t the next occurrence of each character. This lets us jump directly to the next matching character rather than scanning. The preprocessing takes O(26 * m) time and space, but each subsequence query then takes only O(n) time regardless of the length of t.
Algorithm
- Build a 2D array
storeof sizem x 26. Entrystore[j][c]holds the smallest index>= jwhere charactercappears int. - Fill
storefrom right to left:- Initialize
store[m-1]withm + 1for all characters exceptt[m-1]. - For each position
jfromm-2to0, copystore[j+1]and update the entry fort[j].
- Initialize
- To check if
sis a subsequence:- Start at
j = 0. - For each character in
s, look up its next position fromstore[j]and jump to it. - If the jump goes beyond
m, returnfalse.
- Start at
- Return
trueif all characters are matched.
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
n, m = len(s), len(t)
if m == 0:
return n == 0
store = [[m + 1] * 26 for _ in range(m)]
store[m - 1][ord(t[m - 1]) - ord('a')] = m - 1
for i in range(m - 2, -1, -1):
store[i] = store[i + 1][:]
store[i][ord(t[i]) - ord('a')] = i
i, j = 0, 0
while i < n and j < m:
j = store[j][ord(s[i]) - ord('a')] + 1
if j > m:
return False
i += 1
return i == nTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the length of the string .
Common Pitfalls
Confusing Subsequence with Substring
A subsequence does not require consecutive characters, only that the order is preserved. For example, "ace" is a subsequence of "abcde", but "aec" is not because the order is violated. Make sure to only advance the pointer in s when characters match, not when looking for contiguous matches.
Forgetting to Handle Empty String Cases
When s is empty, it is always a subsequence of any string t (including an empty t). When t is empty but s is not, the answer is always false. Failing to handle these edge cases can lead to index-out-of-bounds errors or incorrect results.
Sign in to join the discussion