字符串匹配问题
题目:如果文本中有一个长度为n的源字符串s,一个长度为m的匹配字符串d。(m<=n)请求出字符串s是否包含字符串d。
暴力解法:
思路:将源字符串s固定,将匹配字符串d对齐s的头部,如果s[i]==d[j],就i++,j++。如果发现不匹配,则将匹配字符串d往右移一位,继续上述操作。
如图:
int main(){
string s;//源字符串
string d;//匹配字符串
cin>>s;
cin>>d;
int n=s.length();
int m=d.length();
int j=0,i=0;
int mark=0;//mark用来记录d应该移动次数
while(mark<=n-m) {
if(s[i]==d[j]){//同时向右移动。
j++; i++;
}
else if(s[i]!=d[j]){
j=0;//d串从起始点开始匹配
mark++; //记录s串已经移动多少次
i=mark;
}
if(j==m)
break;
}
if(j==m)//完全匹配
cout<<"匹配成功"<<endl;
else
cout<<"匹配失败"<<endl;
return 0;
}
由于d串每一次都要逐一比较,时间复杂度为O(m),s串移动的次数为O(n-m),时间复杂度为O(m(n-m))。
KMP算法
kmp算法是一种可以将字符串匹配时间复杂度降到线性级的算法。kmp的核心是:找到匹配串d的每一个字符前的最长前-后缀(前缀和后缀的最大公共长度)。
前缀:除最后一个字符外的其余字符。
后缀:除第一个字符外的其余字符。
当s[0]!=d[0],d串应该往右移一位。
当右移动一位的时候,会发现s[5]!=d[5],那么如果是朴素算法,此时d串应该往右移动一位,继续从d[0]开始与s串比较。但是我们会发现d串的第五个字符前为baba,前缀为bab,后缀为aba,其最长前-后缀为ba。
如果d串直接向右移动2位,则才有可能重新匹配。他的目的就是将前缀移到原来后缀所在的位置!原因很简单,因为后缀那一部分跟源字符串s是匹配的,所以此时移动后的前缀与源字符串s也是匹配的。
所以接下来的核心就是求出d的每一个字符前的最长前-后缀。
假设在i字符前的字符串的最长前缀为A,最长后缀为B,A==B。
在这里设置一个数组next[i]来记录i之前的(不包括i)的前后缀的最大公共长度。
如果此时d[i]==d[k],则next[i+1]=next[i]+1=k+1;
如果此时d[i]!=d[k],那么我们应该在B串中找出最长前-后缀。(其实与上述的分析是一样的,只是类比)
假设B串是最长前缀为C,最长后缀为D,C==D。
那么将C移动到D所在位置。
如果d[k]==d[j],则next[k+1]=next[k]+1=j+1;
如果d[k]!=d[j],则应该在D串中找到最长前后缀,然后再移动D串。即重复以上步骤。
结束的标志是当最长前后缀长度为0,即不可再划分。
void getFail(string d,int* next){//查找最长前后缀
next[0]=0;next[1]=0;
int m=d.length();
for(int i=1;i<m;i++){//i从1开始,因为代表第一个字符前的最长前后缀
int j=next[i];//j等于在i字符之前最长前-后缀长度
while(j&&(d[i]!=d[j]))//当最长前后缀长度不为0,需要再次划分最长前后缀
j=next[j];
if(d[i]==d[j])
next[i+1]=j+1;
else //即最长前后缀已不可再划分,长度为0
next[i+1]=0;
}
for(int i=0;i<=m;i++){
cout<<next[i]<<" ";
}
}
找出匹配串d的最长前后缀之后,我们开始与源字符串s进行匹配。
匹配思路为:s串不用进行回溯,即每次i++。d串会进行回溯,不断的划分其最长前后缀,直到最长前后缀为0。
如果最长前后缀为0,说明当前的s[i]无法与d串匹配,则i++,再开始寻找。
int main(){
string s;//源字符串
string d;//匹配字符串
cin>>s;
cin>>d;
int n=s.length();
int m=d.length();
memset(next,0,sizeof(next));//数组初始化为0
getFail(d,next,m);
int j=0;
for(int i=0;i<n;i++){
while(j && s[i]!=d[j])//如果匹配不成功
j=next[j];//找出d串的下一个最长前后缀。
if(s[i]==d[j])
j++;
if(j==m)//完全匹配
cout<<"匹配成功"<<endl;
}
return 0;
}
总结
1.源字符串s不回退,匹配字符串d回退。
2.next[i]记录的是i之前的最长前-后缀。
3.结束标志是最长前后缀为0。