深入浅出KMP

字符串匹配问题

 题目:如果文本中有一个长度为n的源字符串s,一个长度为m的匹配字符串d。(m<=n)请求出字符串s是否包含字符串d。


暴力解法:

思路:将源字符串s固定,将匹配字符串d对齐s的头部,如果s[i]==d[j],就i++,j++。如果发现不匹配,则将匹配字符串d往右移一位,继续上述操作。

如图:

可以发现s[0]==d[0],则i++,j++。当s[2]!=d[2]时,则t向右移动一位继续寻找。如下图


如果在过程中,d与s完全匹配,则退出。若没有匹配,直到t与s右对齐时停止匹配。
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。




  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值