给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。
示例 1:
输入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出: true示例 2:
输入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出: false来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/interleaving-string
有没有人和我一样,拿到这道题后首先想到的是双指针法+递归。
指针 p1开始指向 s1 的头部,指针 p2一开始指向 s2的头部,指针 p3 指向 s3的头部,每次观察 p1和 p2指向的元素哪一个和 p3指向的元素相等,相等则匹配并后移指针,遇到相同字符,采用递归处理分支问题。
那这里我们就先用这样的思路做一下:
#include
#include
using namespace std;
bool isInterLeave_1(char* s1, char* s2, char* s3){
//取字符串长度
int n = strlen(s1),m = strlen(s2),t = strlen(s3);
if(n + m != t) return false;
int i,j,k;
for(i = 0,j = 0,k = 0;k < t;){
//当要s3要匹配的字符与s2、s1当前字母均相同时,问题开始向两个分支走去
if(s3[k] == s1[i] && s3[k] == s2[j]){
//相同取s1
if(isInterLeave_1(s1+i+1,s2+j,s3+k+1))
return true;
//相同取s2
else if(isInterLeave_1(s1+i,s2+j+1,s3+k+1))
return true;
//匹配失败返回false
else return false;
}
//s1当前字符与s3匹配
if(s3[k] == s1[i] && s3[k] != s2[j]) {k++;i++;}
//s2当前字符与s3匹配
else if(s3[k] == s2[j] && s3[k] != s1[i]) {k++;j++;}
//没有符合匹配的字符跳出
else if(s3[k] != s1[i] && s3[k] != s2[j]) break;
}
//字符串完全匹配返回true
if(i == n && j == m && k == t) return true;
else return false;
}
int main(){
char s1[6] = "aabcc", s2[6] = "dbbca", s3[11] = "aadbbcbcac";
cout<
程序运行结果如下:
这种办法当然是可行的,但在力扣上运行有一个示例不能通过。很明显出题官本意是不想让我们通过这种方式来解决这个问题。
我们考虑一种最坏的情况,就是:
输入: s1 = "aaaaa", s2 = "aaaaa", s3 = "aaaaaaaaab" 输出: false
此时的递归树是一棵二叉树,递归列举出所有的匹配情况,最后发现无法匹配成功。
for循环里的时间复杂度就类似于最简单的最用递归实现的斐波那契数列。T(n)=O(2^n)
整个程序的时间复杂度变为T(n)=O(t * 2^n)≈O(n*2^n),空间复杂度为O(n)
也就是说超时了,此时我们想到用动态规划来解决这个问题。
我们应该形成一种思路,字符串问题优先考虑动态规划。
动态规划做多了之后写起来很简单,时间、空间复杂度又低。
我们定义 f(i,j) 表示 s1 的前 i个元素和 s2 的前 j个元素是否能交错组成 s3的前 i+j 个元素。
如果 s1的第 i 个元素和 s3 的第 i+j 个元素相等,那么 s1 的前 i 个元素和 s2 的前 j 个元素是否能交错组成 s3 的前 i+j 个元素取决于 s1 的前 i−1 个元素和 s2 的前 j 个元素是否能交错组成 s3 的前 i+j−1 个元素,即此时 f(i,j)取决于 f(i−1,j),在此情况下如果 f(i−1,j) 为真,则 f(i,j) 也为真。
同样的,如果 s2 的第 j个元素和 s3的第 i+j 个元素相等并且 f(i,j−1) 为真,则 f(i,j) 也为真。
所以状态转移方程可以写做:
f(i,j)=[ f(i−1,j) and s1(i−1) = s3(p) ] or [ f(i,j−1) and s2(j−1) = s3(p)]
p=i+j−1。
边界条件为 f(0,0)=true。
代码如下:
#include
#include
using namespace std;
bool isInterLeave_2(string s1,string s2,string s3){
//取字符串长度
int n=s1.size(),m=s2.size(),t=s3.size();
if(n+m!=t) return false;
//状态转移数组
vector > dp(n+1,vector(m+1));
dp[0][0]=true;
for(int i=0;i<=n;i++){
for(int j=0;j<=m;j++){
int p=i+j-1;
if(i>0){
//这里使用||运算的原因是因为数组是以false赋值的
dp[i][j]=dp[i][j]||(dp[i-1][j]&&s1[i-1]==s3[p]);
}
//这里的||除了上述作用外还有s2的匹配记录不影响s1匹配记录的作用
if(j>0){
dp[i][j]=dp[i][j]||(dp[i][j-1]&&s2[j-1]==s3[p]);
}
}
}
return dp[n][m];
}
int main(){
string s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac";
cout<
运行结果如下:
这里我们把两个dp数组输出看一看:
不难看出这个实现的时间复杂度和空间复杂度都是 O(nm)。
使用滚动数组优化空间复杂度。 因为这里数组dp的第 i 行只和第 i −1 行相关,所以我们可以用滚动数组优化这个动态规划,这样空间复杂度可以变成 O(m)。
#include
#include
#include
using namespace std;
bool isInterLeave_3(string s1,string s2,string s3){
int n=s1.size(),m=s2.size(),t=s3.size();
if(n+m!=t) return false;
vector dp(m+1);
dp[0]=true;
for(int i=0;i<=n;i++){
for(int j=0;j<=m;j++){
int p=i+j-1;
if(i>0){
//这里dp数组作为纵向的继承,通过&&来推断s1当前位和s3是否相等
//不相等的话我置0,要么你s2来完成置1,要么字符串匹配失败
//注意这里这样写是因为每外循环一次就用舍弃上一个外循环的dp数组
dp[j]=dp[j]&&(s1[i-1] == s3[p]);
}
if(j>0){
//这里的||的效果在于s2和s3匹配,不会影响到s1保留的记录
//s2与s3的匹配是通过横向往后延来记录的
dp[j]=dp[j]||(dp[j-1]&&s2[j-1] == s3[p]);
}
}
}
for(int j=0;j<=m;j++){
cout<
状态数组如下,维护一个长为m的数组,每一行利用上一行的数组空间。
时间复杂度:O(nm)。
空间复杂度:O(m),即 s2的长度。