问题描述:
- 给定字符串
s
和t
,判断s
是否为t
的子序列。- 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,
ace
是abcde
的一个子序列,而aec
不是)。
- 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,
- 进阶:
- 如果有大量输入的
S
,称作S1, S2, ... , Sk
,其中k >= 10^9
(10 亿),你需要依次检查它们是否为T
的子序列。在这种情况下,你会怎样改变代码?
- 如果有大量输入的
核心思路:
- 暴力解法就是双指针遍历,这种方法是带有贪心思想的,也就是对于
s
对应位置的字符,都选择t
中对应位置字符第一次出现的位置。 - 但对于进阶问题来说,双指针解法就显得太慢了。
- 动态规划解法就是在遍历
s
前,先预先处理t
串,这种思想和 KMP 字符匹配的思想很相似,将长字符串中的信息提取出来供后续使用。 - 实际上就是要考虑什么样的信息对于
s
来说是有效的就可以了,所需要的信息就是遍历s
中字符,待匹配字符s[i]
在长字符串t
中下一次出现的位置是多少。 - 假如长字符串
t
的长度为n
,建立一个n*26
大小的矩阵dp
,表示每个位置上26
个字符下一次出现的位置,也就是说dp[i][c]
代表在t[i]
位置上下一个匹配字符c
的索引。 - 得到
dp
数组之后,只需要遍历s
串,过程中从dp
数组中获得对应字符的索引即可。【可以注意到该方法其实也是有贪心思想,dp
数组保留的是下一个字符的位置,因为当前只需要跳到下一个匹配的字符即可】
- 动态规划解法就是在遍历
- 还有一种二分搜索的做法。
- 首先仍然是预处理字符串
t
,但和动态规划做法不同,只需要保存每个字符存在的索引即可。 - 接着外部初始化一个索引位置
p = -1
用作后续的遍历;遍历s
串,因为每一个字符都保存了一系列位置,所以可以用二分搜索的方法获得比p
索引大的第一个位置,如果找到这个位置idx
,就将p
更新为idx
。
- 首先仍然是预处理字符串
代码实现:
- 动态规划解法代码实现如下:【代码参考自网友题解,和官方题解不一样,该方法有其他的一些处理,如先在
t
串前面加多一个空字符,原因可以参考注释或参考下面给出链接】class Solution { public: bool isSubsequence(string s, string t) { t = " " + t; // 加多一个字符,方便后续 dp[0][~] 的准确性 int m = s.size(), n = t.size(); vector<vector<int>> dp(n, vector<int>(26, 0)); for(char c = 'a'; c <= 'z'; ++c) { int next_pos = -1; // 表示接下来再不会出现该字符 for(int i = n-1; i >= 0; --i) { // 从后往前遍历 dp[i][c-'a'] = next_pos; if(t[i] == c) // 如果当前位置和 c 相等,则更新 next_pos,给前面的 dp[i-1][c] 使用 next_pos = i; } } int idx = 0; for(char& c : s) { idx = dp[idx][c-'a']; // 找到下一个位置 if(idx == -1) return false; } return true; } };
- 二分搜索解法代码实现如下:
class Solution { public: bool isSubsequence(string s, string t) { vector<vector<int>> pos(26); for (int i = 0; i < t.size(); ++i) pos[t[i] - 'a'].push_back(i); if (s.size() > t.size()) return false; int p = -1; for (char c : s) { auto &ps = pos[c - 'a']; auto it = upper_bound(ps.begin(), ps.end(), p); if (it == ps.end()) return false; p = *it; } return true; } };