算法—KMP字符串匹配

算法—KMP字符串匹配

现在有一个问题,要从一个字符串中查找出指定子串的位置(初始下标),通常地,我们会使用朴素的字符串匹配算法,如下面这道题
给出主串和需要查找的子串,输出子串是否存在,并输出子串的首位在主串中的下标

《算法—KMP字符串匹配》

  • 首先,在主串中设下标i,在字串中设下标j,均从0开始

《算法—KMP字符串匹配》

这时,将子串第一位与主串的第一位进行比较,结果是不同的,那么尽然不同,子串初始位置就一定不在当前的i处,所以i++,j还是0

《算法—KMP字符串匹配》

i=1时,主串当前位置字符仍然与子串中的第一位不同,由上可得,i继续加一,j还是0

《算法—KMP字符串匹配》

i=2时,匹配成功,这时i++,j++,继续判断下一位是否还能匹配

《算法—KMP字符串匹配》

i=3时,仍旧匹配成功,重复上述操作

《算法—KMP字符串匹配》

i=4,仍旧匹配。这时我们发现,i还没有到达主串的末尾,但是j已经到达子串的末尾。这就是查找成功的标志,返回i-j=2就是子串起始在主串中的下标

  • 下面是这种朴素字符串匹配算法的代码
#include<stdio.h>
#include<string.h>

void search(char str[], char son[],int pos)
{
	int index;
	int i = pos;	//主串中的位置下标 
	int j = 0;	//字串中的位置下标
	int len1 = strlen(str);
	int len2 = strlen(son);
	
	while(i < len1 && j < len2)
	{
		if(str[i] == son[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 1;
			j = 0;
		}
	}
	if(j = len2)
	{
		if(pos >= len1-len2 || i >= len1)
		{
			printf("不存在子串了\n");
			return;
		}
		printf("查找到子串,起始索引在主串的%d处\n",i-len2);
		search(str,son,i-len2+1);				//递归继续搜索 
	}
}

int main()
{
	char str[1000];
	char son[1000];
	gets(str);
	gets(son); 
	search(str,son,0);
	return 0;
}

上面的字符串查找确实简单易懂,但有时它却有着很大的缺点,我们看下面的这个例子

《算法—KMP字符串匹配》

  • 现在我们要从上面的主串中查找相对应的子串,我们按之前的朴素字符串查找过一遍

《算法—KMP字符串匹配》

在 i=5,j=5时,判断字符不相符,这时,我们的i就需要回溯到i=1(第一个位置),j需要回溯到j=0(子串起始),然后继续开始如下的几个判断

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

  • 这个时候你会发现,i居然又回到了5,而且字符匹配也没有丝毫进展!那么上面做的那几步岂不是无用功?为什么这么说呢?
我们仔细观察,子串的每一位的字符都是不同的,所以在第一次匹配到i=5,j=5

时,我们是已经知道主串的前五位是和子串的前五位相同的(也就是说主串的前五位也各不相同),所以中间的这四次比较都是可以省略的,我们可以直接跳到最后一步。

有人说这个例子太特殊? 那我们下面再看一个例子

《算法—KMP字符串匹配》

我们继续使用朴素字符串查找

《算法—KMP字符串匹配》

i=j=5时停下,i回溯到i=1,j回溯到0

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

  • 这次我们又发现了什么? i又回到了5,j停留在2。在第一次比较到i=j=5时,我们已经知道主串和子串的前五位相同,也就是说,我们知道了主串的前三位各不相同,但主串的第一位和第四位相同,第二位和第五位相同,那么其实这中间的几步都是可以省略的,我们可以直接让i=5,j=2.
综上,我们发现主串中的i值是在不断回溯的,但最终又会回到原来的值,那么我们干脆不让它回溯了,即当判断主串与子串字符不同时,不改变i的值。
既然i值不回溯,也就是不可以变小,那么要考虑变化的就是j值了。通过观察也可以发现,我们屡次提到了字串的首字符与自身后面字符的比较,发现如果有相同字符,j值的变化就会不同,其实这个j值的变化与主串没关系,关键取决于子串的结构中是否有重复的问题。
  • 在上面的第一个例子中,子串=”abcdex”时,当中没有任何相同的字符,所以j就由5变成了0。第二个例子中,子串=“abcabx”,前缀的 “ab” 与后面的 “ab”是重复的,j就由5变成了2。因此我们可以得出规律:j值的的多少取决于当前字符之前的串的前后缀的相似度

现在就是整个KMP算法最核心的地方了,我们把子串各个位置的j值变化定义为一个数组next,那么next的长度就是子串的长度。我们有这样的函数定义:

  • 当j=0时,next[j]=0
  • 当{k| 1 < k < j 且 ‘p(1) … p(k-1)’ = ‘p(j-k+1) … p(j-1)’}此集合不为空时,next[j]=Max(k)
  • 其他情况,next[j]=1

接下来,我们使用KMP算法的时候,只需要不让i回溯,通过求得子串的next数组,确定每个位置应该回溯到的j值,就能大大提高查找效率了,我们以上面的两个题为例子

《算法—KMP字符串匹配》

子串=“abcdex”
  • j=0时:next[0]=0
  • j=1时:next[1]=1
  • j=2时:2之前的串中没有对称前后缀,next[2]=1
  • j=3时:同上
  • j=4时:同上
  • j=5时:同上
最终next数组={0,1,1,1,1,1};

这次,当我们判断到这一步时,

《算法—KMP字符串匹配》

i不变,令j回溯到(next[j]-1),即i=5,j=0,我们直接来到了这一步:

《算法—KMP字符串匹配》

第二个例子:

《算法—KMP字符串匹配》

子串=“abcabx”
  • j=0时:next[j]=0
  • j=1时:next[j]=1
  • j=2时:2之前的串中无对称的前后缀,next[2]=1
  • j=3时:3之前的串中无对称的前后缀,next[3]=1
  • j=4时:4之前的串中,第一位 ‘a’和第四位 ‘a’相同,故next[4]=2
  • j=5时:5之前的串中,前两位 “ab”和后两位 “ab”相同,故next[5]=3
最终next数组={0,1,1,1,2,3}

那么我们在第一次判断后:

《算法—KMP字符串匹配》

遵守KMP算法的规则,i不回溯,即i仍然等于5,j这时等于5,则应该回溯到(next[j]-1)=2处,我们就直接到了这一步:

《算法—KMP字符串匹配》

是不是又直接跳过了中间的几步? 的确很神奇,这大大提高了字符串查找的效率

下面是KMP字符串匹配算法的代码:
#include<stdio.h>
#include<string.h>

void get_next(char son[],int next[])
{
	int i,j;
	int len = strlen(son);
	i = 0;
	j = -1;
	next[0]=0;
	while(i < len)
	{
		if(j == -1 || son[i] == son[j])
		{
			i++;
			j++;
			next[i] = j + 1;
		}
		else
			j = next[j] -1;			//j回退到合适的位置,i不变 
	}
}

void KMP(char str[],char son[],int pos)
{
	int i = pos;	//主串中的位置索引 
	int j =-1;		//子串中的位置索引
	
	int len1 = strlen(str);
	int len2 = strlen(son);
	
	int next[1000];	//next数组
	get_next(son,next);
	
	while(i < len1 && j <len2)
	{
		if(j == -1 || str[i] == son[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j] - 1;
		}
	}
	if(j = len2)
	{
		if(pos >= len1-len2 || i >= len1)
		{
			printf("不存在子串了\n");
			return;
		}
		printf("查找到子串,起始索引在主串的%d处\n",i-len2);
		KMP(str,son,i-len2+1);
	}
}

int main()
{
	char str[1000];
	char son[1000];
	gets(str);
	gets(son);
	
	KMP(str,son,0);
	return 0;
} 
按理说这里应该结束了,但KMP算法并不是完美的,比如下面这种情况:

《算法—KMP字符串匹配》

子串=“aaaaax”
  • j=0时:next[0]=0
  • j=1时:next[1]=1
  • j=2时:’a’与 ‘a’对称 , next[2]=2
  • j=3时: “aa” 与”aa”对称 next[3]=3
  • j=4时:“aaa” 与”aaa”对称, next[4]=4
  • j=5时:“aaaa” 与”aaaa”对称, next[5]=5
next数组为:{0,1,2,3,4,5}

《算法—KMP字符串匹配》

然后i不变,令j根据所求的next数组回溯

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

《算法—KMP字符串匹配》

我们发现,j又回到了0,我们知道子串中的前五位是相同的,那我们为什么还要一次次的将其与主串中的 ‘b’进行比较呢? 所以我们还需要对KMP算法进行优化
  • 上面的例子中,我们可以直接来到最后一部,即i=5,j=0.所以在next数组中,第二三四五位的next值应该与第一位的next值相等。因此我们需要对求next数组的算法进行优化。

规则:在计算出next数组的同时,如果a字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值。如果不等,则该a位的nextval值就是它自己a位的next值

以上面的这个题为例

《算法—KMP字符串匹配》

next数组为:{0,1,2,3,4,5}
  • j=0时:nextval[0]=0
  • j=1时:next指向j=0,串中j=1与j=0位置的字符相同,nextval[1]=nextval[0]=0
  • j=2时:next指向j=1,串中j=2与j=1位置的字符相同,nextval[2]=nextval[1]=0
  • j=3时:next指向j=2,串中j=3与j=2位置的字符相同,nextval[3]=nextval[2]=0
  • j=4时:next指向j=3,串中j=4与j=3位置的字符相同,nextval[4]=nextval[3]=0
  • j=5时:next指向j=4,串中j=5与j=4位置的字符不同,nextval[5]=next[5]=5

《算法—KMP字符串匹配》

这时直接回溯到j=-1,然后

《算法—KMP字符串匹配》

下面是优化过的KMP算法的代码:
#include<stdio.h>
#include<string.h>

void get_nextval(char son[],int nextval[])
{
	int i,j;
	int len = strlen(son);
	i = 0;
	j = -1;
	nextval[0] = 0;
	while(i < len)
	{
		if(j == -1 || son[i] == son[j])
		{
			i++;
			j++;
			if(son[i] != son[j])	//改进部位:若当前字符与前缀字符不同 
			{
				nextval[i] = j + 1;		//则当前的j为nextval在i位置的值 
			}
			else
				nextval[i] = nextval[j];	//如果与前缀字符相同,则将前缀字符的nextval值赋值给nextval在i位置的值
		}
		else
			j= nextval[j] -1;
	}
}

void KMP(char str[],char son[],int pos)
{
	int i = pos;	//主串中的位置索引
	int j =-1;		//子串中的位置索引
	
	int len1 = strlen(str);
	int len2 = strlen(son);
	
	int next[1000];	//next数组
	get_nextval(son,next);
	
	while(i < len1 && j <len2)
	{
		if(j == -1 || str[i] == son[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j] - 1;
		}
	}
	if(j = len2)
	{
		if(pos >= len1-len2 || i >= len1)
		{
			printf("不存在子串了\n");
			return;
		}
		printf("查找到子串,起始索引在主串的%d处\n",i-len2);
		KMP(str,son,i-len2+1);
	}
}

int main()
{
	char str[1000];
	char son[1000];
	
	gets(str);
	gets(son);
	
	KMP(str,son,0);
	return 0;
} 
    原文作者:KMP算法
    原文地址: https://blog.csdn.net/wintershii/article/details/81661447
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞