名词解释
主串:一段字符串。eg:S=‘moshichuanpipei’
子串:主串中连续的一部分。eg:‘moshichuan’、‘pipei’
模式串:一段字符串,实际场景中远短于主串。eg:‘shich’、‘pipep’
模式串匹配:在主串中找到与模式串相同的子串,并返回其(第一次)出现位置。
前缀:除最后一个字符外,字符串的所有头部子串
后缀:除第一个字符外,字符串的所有尾部子串
部分匹配值:最长相等的前后缀长度
朴素算法
算法思想:如果我们想要在主串S='moshichuanpipei’中找到模式串T='shich’或T=‘pipep’,那么最直接且简单的方法就,遍历S的所有长度和T相同的子串,寻找是否有相同的
算法实现:
int Index(SString S,SString T){
int i=1,j=1; //i,j分别指向S和T将要比较的字符
while(i<=S.length && j<=T.length){
if(S.ch[i]==T.ch[j]){
i++;j++; //比较后继字符
}
else{
i=i-j+2;j=1;//指针后移开始下一轮匹配
}
}
if(j>T.length) //匹配成功,返回第一次出现起始位置
return i-T.length;
else //匹配失败
return 0;
}
分析复杂度:假设主串长度为15,模式串长度为5,最好情况为:第一个子串就匹配成功eg:S=‘aaaabaaaaaaaaaa’,T=‘aaaab’,最坏情况为:比较了所有长度为5的子串也没能和模式串匹配,且模式串和每个子串都比较了5个字符,eg:S=‘aaaaaaaaaaaaaaa’,T=‘aaaab’,
抽象表示主串长n,模式串长m,在匹配过程中字符比较的时间为O(1),那么最好时间复杂度为O(m),最坏时间复杂度为O(n*m);
在朴素算法中遇到较坏情况时S的指针i需要多次回溯,导致S与T串有很多字符多次没必要的重复比较,浪费了计算时间,所以我们需要更优的算法(KMP算法)来应对这种情况
KMP算法
算法目的:对于S=‘aaaaaaaaaaa’,T=‘aaaab’这种情况,
第一轮匹配时面对的情况是S=’???..??’,T=’???’,匹配后发现S与T前4个字符相同,在第5个字符不同,此轮匹配失败
第二轮匹配时面对的情况是S=‘aaaaa???..??’,T=‘aaaab’,此时通过观察在下一轮匹配时我们希望能将S的第六个子串与T匹配,而不是第二个。这就是KMP算法的目标。
算法思想:可以发现若要实现上述目的,匹配失败S的指针不需要回溯,而是改变T的指针使模式串向后移动,
即
第一轮结束后 第二轮开始时
i i
S aaaaa???... S aaaaa???...
T aaaab T aaaab
j j
这样移动的原因是aaaaa长为4的后缀与aaaab长为4的前缀匹配成功。
细心话的会在这里发现两个问题:1.如何计算模式串后移几位(求next数组);2.仍然有一次重复匹配(KMP的优化)
这一小节主要是理解KMP思想,所以先带着两个问题阅读,
枚举求next数组:
第5个字符不匹配
i i
S aaaaa???... S aaaaa???...
T aaaab T aaaab
j j 向后移动1位,使j=4
第4个字符不匹配
i i
S aaac????... S aaac????...
T aaaa? T aaaa?
j j 向后移动1位,使j=3
第3个字符不匹配
i i
S aac?????... S aac?????...
T aaa?? T aaa??
j j 向后移动1位,使j=2
第2个字符不匹配
i i
S ac??????... S ac??????...
T aa??? T aa???
j j 向后移动1位,使j=1
第1个字符不匹配
i i
S c???????... S ca??????...
T a???? T a????
j j 向后移动1位,使j=1,i++
定义next数组为:当S[i]!=T[j]使,要使j=next[j],来实现模式串的移动
由于每个人字符串定义方式不同,所以求得的next数组也会不同,但都是为了改变j,来移动模式串到合适的位置
此时求得next数组为
T | a | a | a | a | b |
---|---|---|---|---|---|
j | 1 | 2 | 3 | 4 | 5 |
next[j] | 0 | 1 | 2 | 3 | 4 |
此处next[1]=0是为了与next[2]区分,根据枚举两者都要j=1,但前者还需要i++,为了方便代码编写使其值为0
综上所述,KMP算法的原理就是,主串不动,通过一个辅助数组next[],来实现不同情况下模式串的移动,以减少重复匹配的次数
当前将next作为已知信息,来完成KMP算法的实现
int Index_KMP(SString S,SString T,int next[]){
int i=1,j=1;
while(i<=S.length && j<=T.length){
if(j==0||S.ch[i]==T.ch[j]){
i++;j++; //比较后继字符
}
else
j=next[j]; //更新指针开始下一轮匹配
}
if(j>T.length) //匹配成功,返回第一次出现起始位置
return i-T.length;
else //匹配失败
return 0;
}
与朴素算法的区别只在与匹配失败后如何更新指针。
分析复杂度:主串长n,模式串长m,在匹配过程中字符比较的时间为O(1),求解next数组时间为O(m),那么最好时间复杂度为O(m),最坏时间复杂度为O(n+m);
理解以上过程那么就理解了KMP算法的原理,即通过改变更新指针的方法来减少重复比较次数,其核心在于求解和利用辅助数组next[]
求解next[]
观察之前枚举的情况可以发现,
未完待续。。。
在一般情况下朴素算法的时间复杂度也会近似为O(n+m)。eg:S=‘fffffabcde’ T=‘abcde’
完整测试代码
#include<bits/stdc++.h>
using namespace std;
#define MANLAN 255
typedef struct{
char ch[MANLAN];
int length;
}SString;
int Index_KMP(SString S,SString T,int next[]);//KMP匹配
int Index(SString S,SString T);//暴力匹配
//赋值操作。把串T赋值为chars
void StrAssian(SString &T,char chars[]){
T.length=0;
while(chars[T.length]!='\0')
T.ch[++T.length]=chars[T.length-1];
}
//int next[MANLAN];
int main(){
SString S,T;
char chars1[]="abcabcacbab";
char chars2[]="abcac";
int next[]={-1,0,1,1,1,2}; //模式串对应的next数组
StrAssian(S,chars1);
StrAssian(T,chars2);
cout<<"S:";
for(int i=1;i<=S.length;i++)cout<<S.ch[i];cout<<endl;
cout<<"T:";
for(int i=1;i<=T.length;i++)cout<<T.ch[i];cout<<endl;
printf("%d\n",Index(S,T));
printf("%d\n",Index_KMP(S,T,next));
return 0;
}
int Index_KMP(SString S,SString T,int next[]){
int i=1,j=1;
while(i<=S.length && j<=T.length){
if(j==0||S.ch[i]==T.ch[j]){
i++;j++; //比较后继字符
}
else{
j=next[j]; //更新指针开始下一轮匹配
}
}
if(j>T.length) //匹配成功,返回第一次出现起始位置
return i-T.length;
else //匹配失败
return 0;
}
int Index(SString S,SString T){
int i=1,j=1; //i,j分别指向S和T将要比较的字符
while(i<=S.length && j<=T.length){
if(S.ch[i]==T.ch[j]){
i++;j++; //比较后继字符
}
else{
i=i-j+2;j=1;//指针后移开始下一轮匹配
}
}
if(j>T.length) //匹配成功,返回第一次出现起始位置
return i-T.length;
else //匹配失败
return 0;
}