【数据结构】模式串匹配

串详解含统考真题

名词解释

主串:一段字符串。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						  iS aaaaa???...			S aaaaa???... T aaaab					T  aaaabj						  j

这样移动的原因是aaaaa长为4的后缀与aaaab长为4的前缀匹配成功

细心话的会在这里发现两个问题:1.如何计算模式串后移几位(求next数组);2.仍然有一次重复匹配(KMP的优化)
这一小节主要是理解KMP思想,所以先带着两个问题阅读,

枚举求next数组:

5个字符不匹配i						  iS aaaaa???...			S aaaaa???... T aaaab					T  aaaabj						  j		向后移动1位,使j=44个字符不匹配i						 iS aaac????...			S aaac????... T aaaa?					T  aaaa?j						 j		向后移动1位,使j=33个字符不匹配i						iS aac?????...			S aac?????... T aaa??					T  aaa??j						j		向后移动1位,使j=22个字符不匹配i					   iS ac??????...			S ac??????... T aa???					T  aa???j					   j		向后移动1位,使j=11个字符不匹配i						   iS c???????...			S ca??????... T a????					T  a????j						   j		向后移动1位,使j=1,i++

定义next数组为:当S[i]!=T[j]使,要使j=next[j],来实现模式串的移动

由于每个人字符串定义方式不同,所以求得的next数组也会不同,但都是为了改变j,来移动模式串到合适的位置

此时求得next数组为

Taaaab
j12345
next[j]01234

此处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++;    //比较后继字符}elsej=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[]

手算next[]

为了理解next求解过程,不妨再用几个模式串枚举得其next[]数组,来观察next数组与模式串的关系

通过枚举得到

T       a a a a b
next    0 1 2 3 4
T       a b c a c
next    0 1 1 1 2
T       a b a a b c a b a
next    0 1 1 2 2 3 1 2 3

此时仍较难发现其中规律,但似乎与模式串的前后缀匹配有关系,可以试试再求出与前后缀匹配有关的部分匹配值,用pm表示

T       a a a a b
PM      0 1 2 3 0
next    0 1 2 3 4T       a b c a c
PM      0 0 0 1 0
next    0 1 1 1 2T       a b a a b c a b a
PM      0 0 1 1 2 0 1 2 3
next    0 1 1 2 2 3 1 2 3

此时可以发现

next[j]=pm[j-1]

若结合之前枚举的过程来看,可以发现

移动位数=已匹配字符-对应的部分匹配值

现在我们就可以不需要通过枚举来求得模式串对应的next数组

以T='abaabcaba’举例

Tabaabcaba
pm001120123

为了向next靠齐,在pm前加一位-1

Tabaabcaba
pm001120123
pm_next-1001120123
next011223123

上表中next为我们需要的辅助数组,因为next时pm_next每一位加1得到的,更新T指针时位数也会差1,所以next用于模式串从1号位置开始存储,而pm_next则可用于模式串从0号位置开始存储的情况,主要理解其计算过程,使用时视情况选择即可

综上所述:手算next数组的方法为,①先求得模式串每一个前缀对应的部分匹配值pm,②再在pm前加一位-1得到pm_next,③最后在pm_next的每一位加1得到next数组。

代码计算next[]

算法目的:T长为m,在O(m)的时间内求得,T对应的next数组。

先尝试模拟手算,发现计算pm值时需要经常比较前后缀,最坏时间复杂度会接近O(m^2),这种思路不可行

重新审视next数组

T       a a a a b
next    0 1 2 3 4
T       a b c a c
next    0 1 1 1 2
T       a b a a b c a b a
next    0 1 1 2 2 3 1 2 3

发现假设S为T[1]到T[j-1]组成的串,那么next[j]=S的最长相等前后缀长度+1,其中next[1]=0;

例如

				j
T       a b a a b c a b a
next    0 1 1 2 2
当j=5时,next[4]='abaa'最长前后缀长度+1=2

核心

				  j
T       a b a a b c a b a
next    0 1 1 2 2
当j=6时求解next[6],已知next[5]=2,根据next性质可以得到'abaa'最长前后缀长度为next[5]-1=1,'a'='a'
问题转换为,已知T1=T4,求'T1T2T3T4T5'最长前后缀长度
此时T5=T2,那么'T1T2T3T4T5'='T1T2T3T1T2'最长前后缀长度2,next[6]=2+1=3
得出,设k=next[j],若Tj=Tk,则next[j+1]=next[j]+1
				    j
T       a b a a b c a b a
next    0 1 1 2 2 3
当j=7时求解next[7],已知next[6]=3,根据next性质可以得到'abaab'最长前后缀长度为next[6]-1=2,'ab'='ab'
问题转换为,已知T1T2=T4T5,求'T1T2T3T4T5T6'最长前后缀长度,
此时T6≠T3,只能得到'T1T2T3T4T5T6'='T1T2T3T1T2T6',又因为next[3]=1,且T6≠T1,算出此时最长前后缀长度0,next[7]=0+1=1
得出,设i=j,k=next[j],若Ti≠Tk,则令j=next[j],k=next[j],比较Ti与Tk直到Ti=Tk或者j=0
此时next[i+1]=next[j]+1

按上诉方法求next数组就不需要重复比较前后缀,只需要利用之前计算得到的next值

算法思想

由上面两条结论

设k=next[j],若Tj=Tk,则next[j+1]=next[j]+1

设i=j,k=next[j],若Ti≠Tk,则令j=next[j],k=next[j],比较Ti与Tk直到Ti=Tk或者j=0,此时next[i+1]=next[j]+1

总结得

计算next[j+1]时,应该设i=j+1,k=next[j],循环比较Ti-1与Tk,若Ti-1=Tk,退出循环;若Ti-1≠Tk,则令j=next[j],k=next[j],直到i-1=Tk或者j=0,退出循环;最后next[i]=next[j]+1;

算法实现

求next[j+1]的实现过程中,用i记录j,用j记录next[j],当j==0或者ch[i]==ch[j],i++,j++,next[i]=j;就相当于next[j+1]=next[j]+1;

void get_next(int next[],SString T){int i=1,j=0;next[1]=0;      //初始化while(i<T.length){if(j==0||T.ch[i]==T.ch[j]){i++,j++;next[i]=j;		//若Ti=Tj,next[i]=next[j]+1}else j=next[j];		//否则令j=next[j]}
}

问题②KMP的优化

回忆朴素算法和kmp算法

int Index_KMP(SString S,SString T,int next[]){int i=1,j=1;		//i,j分别指向S和T将要比较的字符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;
}

两者形式上很相似,唯一的区别在于匹配失败后如何更新指针,所以在一般情况下朴素算法的时间复杂度也会近似为O(n+m)。eg:S=‘fffffabcde’ T=‘abcde’,因此朴素算法至今仍被采用。

为了能进一步优化KMP,先找出它的问题

第一轮结束后			第二轮开始时		i						 iS aaaba???...			S aaaba???... T aaaab					T  aaaabj						 j

按上诉KMP算法在第二轮开始时会比较S[4]与T[3],发现不匹配后比较S[4]与T[2],由发现不匹配后比较S[4]与T[1],之后再比较S[5]与T[1]
在我们有了next数组后知道’aa’最长相等前后缀长度为1形如’T1T1’;‘aaa’最长相等前后缀长度为2形如’T1T1T1’;‘aaaa’最长相等前后缀长度为3形如’T1T1T1T1’;所以我们希望第二轮开始时能直接比较S[5]与T[1]
那么就需要调整更新指针的方法,即优化next数组

出现上述问题原因在于,存在T[j]=T[next[j]],消除这个问题只需要将next[j]修正为next[next[j]],直到两者不相等,令新数组为nextval

算法实现

void get_nextval(int nextval[],SString T){int i=1,j=0;nextval[1]=0;      //初始化while(i<T.length){if(j==0||T.ch[i]==T.ch[j]){i++,j++;if(T.ch[i]!=T.ch[j]) nextval[i]=j;else nextval[i]=nextval[j];}else j=nextval[j];}
}

完整测试代码

#include
using namespace std;#define MANLAN 255
typedef struct{char ch[MANLAN];int length;
}SString;int Index(SString S,SString T);//暴力匹配
void get_next(int next[],SString T);//计算next数组
void get_nextval(int nextval[],SString T);//计算nextval数组
int Index_KMP(SString S,SString T,int next[]);//KMP匹配//赋值操作。把串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 Nextval[MANLAN];
int main(){SString S,T;char chars1[]="aaaaaaaaaaaaaaaaaaab";char chars2[]="aaaab";
//	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));get_next(Next,T);printf("%d\n",Index_KMP(S,T,Next));get_nextval(Nextval,T);printf("%d\n",Index_KMP(S,T,Nextval));cout<<"next:";for(int i=1;i<=T.length;i++)cout<<Next[i]<<" ";cout<<endl;cout<<"nextval:";for(int i=1;i<=T.length;i++)cout<<Nextval[i]<<" ";cout<<endl;return 0;}void get_nextval(int nextval[],SString T){int i=1,j=0;nextval[1]=0;      //初始化while(i<T.length){if(j==0||T.ch[i]==T.ch[j]){i++,j++;if(T.ch[i]!=T.ch[j]) nextval[i]=j;else nextval[i]=nextval[j];}else j=nextval[j];}
}void get_next(int next[],SString T){int i=1,j=0;next[1]=0;      //初始化while(i<T.length){if(j==0||T.ch[i]==T.ch[j]){i++,j++;next[i]=j;}else j=next[j];}
}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;
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部