问题描述:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例:
分析:
该算法很容易想到运用双指针分别指向s和t字符串的第一个字符,如果俩指针指向的字符相同,都往后移动一个单位,如果不同,说明没有匹配成功,那么指向s的指针不动,而指向t的指针往后移。流程图如下:
双指针代码:
class Solution {
public:
bool isSubsequence(string s, string t) {
int n=s.size();
int m=t.size();
int i=0;
int j=0;
while(i<n&&j<m){
if(s[i]==t[j]){
i++;
}
j++;
}
return i==n;
}
};
注意到如果我们需要检查非常多的s字符串是否为t的子字符串,那么双指针的算法就不太高效了。所以我们考虑动态规划的方法。
动态规划:
我们可以定义一个二维表dp[i][j]用来表示t字符串从位置i开始往后第j个字母第一次出现的位置,行代表t字符串第i位的字母,列代表26个字母。我们可以通过一个例子来理解。
例:s:"abz" t:"aazbdz"
从后往前遍历t字符串,首先最后一个字母为z,我们检查dp表发现在z后没有其他字母出现,所以我们将dp[5][0~25]都与dp[6][0~25]保持一致(将第六行都初始化为6,因为6已经超过t字符串下标上限,所以可以认为是无穷大,代表t[i]之后没有出现第j个字母),而z第一次出现的位置就是i,所以更新二维表可得上面右图所示。以此类推可以得到最终dp为:
之后我们将s与dp开始比对,s[0]='a',在dp表0行查询得到t字符串中'a'第一次出现的位置是t[0],
因此我们从第一次出现该字母的下一个位置比对s[1],也就是dp表的1行。
s[1]与dp表1行比对得到'b'在t字符串中第一次出现的位置是t[3],所以同理从第一次出现的位置+1的位置比对下一个字母s[2],以此类推。
只要发现s[i]在dp表中相应检查行中为6,则说明该字母在t的剩余字符串中未出现过,我们就返回false。
动态规划代码:
class Solution {
public:
bool isSubsequence(string s, string t) {
int n=s.size(),m=t.size();
vector<vector<int>> dp(m+1,vector<int>(26,0)); //定义一个动态规划二维表,行代表t字符串,列代表26个字母
for(int i=0;i<26;i++) //初始化第m行值为相对意义上的无穷(下标超过t字符串最大下标)
dp[m][i]=m;
for(int i=m-1;i>=0;i--){ //从t字符串后往前记录i位置第j个字母第一次出现的位置
for(int j=0;j<26;j++){
if(t[i]==j+'a'){ //出现了则记录下当前位置
dp[i][j]=i;
}
else{ //未出现则将上一组的状态写入当前状态中
dp[i][j]=dp[i+1][j];
}
}
}
int k=0;
for(int i=0;i<n;i++){ //检查s字符串
if(dp[k][s[i]-'a']==m) //遍历发现没有出现过s[i]字符则返回false
return false;
k=dp[k][s[i]-'a']+1; //否则取该字母第一次出现的位置,并且从其在t字符串第一次出现的位置的下一个位置开始检查s字符串下一个字符
}
return true; //依次遍历完后都能在dp表中查到相应的位置即返回true
}
};