0
点赞
收藏
分享

微信扫一扫

【数据结构】模式串匹配

回溯 2022-01-31 阅读 114

名词解释

主串:一段字符串。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=44个字符不匹配
		 i						 i
	S aaac????...			S aaac????... 
	T aaaa?					T  aaaa?
		 j						 j		向后移动1位,使j=33个字符不匹配
		i						i
	S aac?????...			S aac?????... 
	T aaa??					T  aaa??
		j						j		向后移动1位,使j=22个字符不匹配
	   i					   i
	S ac??????...			S ac??????... 
	T aa???					T  aa???
	   j					   j		向后移动1位,使j=11个字符不匹配
	  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数组为

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++;    //比较后继字符
		}
		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;
}
举报

相关推荐

0 条评论