字符串匹配-KMP算法

1.算法引入

字符串匹配:在主串M里寻找子串S第一次出现的位置
我们第一反应是暴力算法:
但这个算法有个问题,一旦M[i]!=S[j],i就需要往前回溯。如下图:

当主串从M[1]开始与子串匹配,匹配到M[4]时,发现M[4]!=S[3]即失配,此时子串回到S[0],主串回溯本次匹配开始位置下个位置M[2],开启新一轮的匹配。

于是我们提出问题:怎样做可以使主串不用往前回溯呢?
KMP算法可以解决这个问题。

2.核心思想

如果子串S在下标j处与主串M在下标i处失配(即S[j]!=M[i]),若想不回溯i,那么只能对子串S进行一些操作。

KMP:
第一步:在子串中寻找最大的相同前后缀p
前缀front:子串从S[0]到S[0+p1]
后缀behind:子串从失配下标前一个即S[j-1]到S[j-1-p2],S[j-1-p2]~S[j-1]
相同前后缀:S[0]=S[j-1-p2],S[1]=S[j-1-p2+1]......S[0+p1]=S[j-1],则p1=p2=p
最大的相同的前后缀:在若干对相同前后缀里找到最大的p
注意:这里的p是前缀末尾下标,所以0≤p≤j-2,最大相同前后缀长度是p-0+1=p+1
 

第二步:移动子串,匹配S[p+1]与M[i](i是主串失配处的下标)
形象来讲,向右拉动子串,使S[p+1]与M[i]对齐。
(如果p=j-1,子串根本没有移动,会保持着失配的情况,所以p必须<j-1)
注意:当然,也许不存在相同前后缀,此时匹配S[0]与M[i]

一些问题:
①为什么要寻找相同前后缀呢?
        子串在j下标处失配,此时子串中总是存在若干个后缀S[j-1-p]~S[j-1],它们一定与主串M[i-1-p]~S[i-1]相同,如果能在子串中找到前缀front与后缀behind相同,那么前缀S[0]~S[p]一定也与主串M[i-1-p]~M[i-1]相同,此时寻找匹配的子串的进度只要继续从匹配子串S[p+1]与主串M[i]开始即可,而不必回溯i。
②为什么要寻找最大的相同前后缀呢?
        相同前后缀越大,说明子串重新开始匹配的S[p+1]越大,剩下待匹配的字符越少。那我们当然是要找最大的相同前后缀。

举例如下图:

(KMP的核心思想即如此,至于为什么KMP算法成立,感兴趣的伙伴可以自行搜索相关的解释说明)

3.C代码实现

1)预处理next[len2]


根据KMP的核心思想,我们知道子串S一旦在某个下标失配,就需要去找最大p,然后向右拉动子串,使S[p+1]与M[i]对应。也就是说,在进行字符匹配之前,要先确定子串中每个元素失配后应该从哪个下标开始继续与M[i]匹配,也就是确定S[p+1]。我们用next[len2]数组来存储每个元素的S[p+1],len2为子串长度。因为0≤p≤j-2,所以S[0]与S[1]不存在最大p,所以next[0]=next[1]=0;

那么next[len2]应该怎样用程序来实现呢?
        核心思想的第一步寻找最大相同前后缀已经告诉我们,0≤p≤j-2,既然要寻找最大p,不妨从最大的=j-2开始逐个尝试。也就是说当p=j-2时,比较S[0]~S[p](即S[j-2])与S[j-1-p](即S[j-1-(j-2)])~S[j-1],若相同则成功找到最大p,next[j]=p+1=j-2+1;若不相同则p=j-3,比较S[0]~S[p](即S[j-3])与S[j-1-p]](即S[j-1-(j-3)])~S[j-1],以此类推。若直到p=0都没有找到相同的前后缀,则next[j]=0。
        不过,这种方法有个的很大缺点:效率低。因为我们最多要对p进行j-1次的尝试,并且每次尝试都要从比较S[0]与S[j-1-p]开始,也就是每次匹配都要从头开始。
        于是我们想,如果存在一个针对于p的筛选机制,对于保留下来的p,它们有个共同的特点,即对于该p,存在k(0≤k≤p-1),使得要匹配的前后缀有尽可能多的部分相同,即存在S[0]~S[k]==S[j-1-p]~S[j-1-p+k],那么我们只需要比较剩下的S[k+1]~S[p]与S[j-p+k]~S[j-1],程序的效率会得到提高。
        不过这种筛选机制存在一个瑕疵,即被筛选的p一定大于0,因为当p=0时,不存在0≤k≤p-1。
        KMP已经为我们提供了一种筛选机制,并且该机制解决了p一定大于0的问题。

        当子串在下标j失配时,筛选机制如下:
第一个被筛选出的p=next[j-1]
根据next的定义,我们得出S[0]~S[p-1]==S[(j-1)-1-(p-1)]~S[(j-1)-1],则k=p-1,此时待匹配前缀只剩下S[p]而后缀只剩下S[j-1],于是比较二者是否相同,若相同则next[j]=p+1=next[j-1]+1;若不同则继续筛选;
第二个被筛选出的p=next[p],
同样根据next的定义,本来需要比较S[0]~S[p]与S[(j-1)-p]~S[(j-1)]变成只需比较S[p]与S[j-1],若相同,则next[j]=p+1=next[next[j-1]]+1;
......
最后被筛选出的p=next[p]=0,
直接比较S[0]与S[j-1],若相同则next[j]=p+1=1,若不同next[p]=0。

        其实我们可以发现,KMP提供的筛选机制本质上是通过保证S[0]~S[p-1]==S[(j-1)-1-(p-1)]~S[(j-1)-1],去寻找S[p]==S[j-1]。更直观来说,我们把S[0]~S[next[j-1]]当做子串,而把S[0]~S[j-1]当做主串,于是筛选机制就变为:初始已经有p=next[j-1]S[0]~S[p-1]==S[j-1-p]~S~[j-2],判断S[p]与S[j-1],若S[p]!=S[j-1],我们可以理解成子串在下标p处与主串在下标j-1处失配,于是按照KMP算法完成寻找最大前后缀以及移动子串再比较的操作。

2)代码实现    

#include <stdio.h>
#include <stdbool.h>//bool 
#include <stdlib.h>//malloc
int main()
{
	char *M;
	char *S;
	int *next;
	int len1,len2,p;
	int i,j;
	bool flag;
	printf("请依次输入主串与子串长度(以空格分隔):");
	scanf("%d %d",&len1,&len2);
	M=(char *)malloc(sizeof(char)*len1+1);
	S=(char *)malloc(sizeof(char)*len2+1);
	next=(int *)malloc(sizeof(int)*len2+1);
	printf("请依次输入主串与子串(以空格分隔):");
	scanf("%s %s",M,S);
	next[0]=0;
	next[1]=0; 
	//预处理next 
	for(j=2;j<len2;j++)
	{
		p=next[j-1];//初始筛选条件 
		while(1)
		{
			if(S[p]==S[j-1]) 
			{ 
				next[j]=p+1;
				break;
			}
			else 
			{
				if(!p) 
				{
					next[j]=0;//S[0]!=S[J-1]
					break;
				}
				p=next[p];
			}
		}	
	}
	
	//开始匹配子串 
	j=0;
	for(i=0;i<len1;i++)//最坏的情况是M[len1-len2]~S[len1-1]==S[0]~S[len2-1]
	{
		
		flag=0;
		if(M[i]==S[j]) j++;
		else
		{
			while(1)
			{
				j=next[j];
				if(M[i]==S[j]) {flag=1;break;}
				else if(j==0) break;
			}
			if (flag==1) j++;	
		}
		if(j==len2) break}
			
	}
	if(j==len2) printf("\n子串第一次在主串中出现的位置是%d!",i-len2+2);
	else printf("\n子串没有在主串中出现过!"); 
	M=NULL;S=NULL;next=NULL;
	free(M),free(S),free(next) ;	
 } 


        

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值