转载声明:文章来源https://blog.csdn.net/flyinithcolrs/article/details/137381472
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
#include //bool
#include //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 {
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 {
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) ;
}
帖子还没人回复快来抢沙发