KMP算法中next数组的求法分析

在KMP模式匹配算法中,next数组是一个很关键的东西,next[j]表示当模式中第j个字符与主串中相应字符“失配”时,在模式中需重新和主串中该字符进行比较的字符的位置。理解求next数组的算法是一个难点,在本文中,我尽量不使用公式,只通过描述性语句来说明这个算法到底做了什么,是怎么求出的next数组。

next数组的定义方式有两种,第一种是严蔚敏数据结构书中的定义方式,next数组下标为0的地方没有存储数据,下标为1的地方存储模式中第一个字符的next值,其定义方式如下:


与此对应,字符串下标为0的位置存储的是字符串的长度,字符是从下标1处开始存储的。

另一种定义方式实际使用时比较常见,next数组的值是从下标0处开始存储的,与之对应的字符串也是C风格的,以“\0”结尾,此时的next数组对应位置的值比第一种定义中的分别小1,原理相同,这里使用第一种定义。

在第一种定义下,next数组的值有两个含义:

1、对于求next数组的程序,next[j]指 字符串T中j字符之前的字符串 的前缀与后缀匹配的字符数加1

2、对于使用next数组的KMP程序,next[j]指子串中j处字符与主串不相等时,j需回溯到的位置

(结合二者,可知字符串T中j字符之前的字符串的前缀与后缀匹配的字符数加1 的值等于 子串中j处字符与主串不相等时,j需回溯到的位置,至于二者为什么相等,可以参考《算法导论》字符串匹配一章的证明。

对于next数组的求法,如果直接笔算的话并不难写出,

如对于字符串T1,

T1
8abcabcde

其对应的next数组为(下标0处存储字符串长度):

next1
801112341

下面就是我们最常见到的求next数组的算法:

void get_next(String T, int *next)
{
	int i, j;
	i = 1;
	j = 0;
	next[1] = 0;
	while(i < T[0])				//此处T[0]表示串T的长度 
	{
		if(j == 0 || T[i] == T[j]){	//T[i]表示后缀的单个字符 
			i++;		 	//T[j]表示前缀的单个字符 
			j++;
			next[i] = j;
		}
		else
			j = next[j];		//若字符不相同,则j值回溯 
	}
}
为便于对比分析,写出对应的KMP算法程序如下:

int Index_KMP(String S, String T)
{
	int i = 1, j = 1;			//i用于主串S当前位置下标值,j用于子串T 
	int next[MAXSIZE];
	get_next(T, next);
	while(i <= S[0] && j <= T[0]){
		if(j == 0 || S[i] == T[j]){
			i++;
			j++;
		}
		else{
			j = next[j];		//j回溯到合适的位置,i值不变 
		}
	}
	if(j > T[0])
		return i - T[0];
	else
		return 0;
}

我是通过函数运行的过程对get_next函数进行分析的,输入上述的T1字符串,可得i与j的变化过程如下:

get_next中i,j的变化过程
 i 12*3*45677*8
 j 010101234101

T1
8abcabcde
012345678


 其中,*表示j值回溯,i值保持不变,T1的第二行表示数组下标。

结合get_next程序和i,j的变化表,可以发现:i值只增不减,j值由于存在回溯,有增有减,这一点与KMP程序的执行过程很相似;

此外,由程序可知,next[1]=0,next[2]=1是固定的,分别对应于T1[j]前不含和只含一个字符,这两个next值不随字符串T变化,这从定义中也可得出。

在i,j变化表中,除了初始行i=1,j==0和最后一行i=8,j=1,同时含有两数(不含*)的行都执行了语句T[i]==T[j],

也就是说,当T1[i]的前面含有两个或两个以上的字符时,首先对首字符T1[1]和T1[i-1]进行比较,

如果相等,执行语句:

i++;
j++;
next[i] = j;

即i,j同时加1,j代表了前后缀匹配的字符数,并将j值赋给next[i],i,j同时加1相当于它们所指向的位置同时后移一位,然后再执行比较语句T[i]==T[j],

如果仍然相同,进行上述同样的操作;

如果不相等,则执行语句:

                        j = next[j];		//若字符不相同,则j值回溯 
这一句是程序中最核心的语句,在分析之前,先回顾一下j值回溯的过程:

KMP算法中有一句相同的语句,事实上,它们的作用也相同,在朴素的模式匹配算法(即原始的直接匹配算法)中,如果遇到不等的字符,主串与子串都需要回溯,子串的当前字符需回溯到首位,而主串当前字符要回溯到上次匹配首位的下一位,而在KMP中,由于next数组的应用,主串不需要回溯,子串也不一定要回溯到首位,而是根据不相等位的next值进行回溯。

为了便于理解,下面画出字符串匹配过程中,子串回溯的过程:

如下图所示,上面是主串,下面是子串,在x位置处出现了字符不相等:


由于x位置处的next值为3,则在下次匹配过程中,可以直接跳到下标为3的字符处进行比较,即是c的位置,如下图:


虚线表示不需要进行,即被省略的比较。

下面注意力重新回到get_next函数的j = next[j]语句,它利用前面已经求得的next值进行回溯,以找到当前字符不相等时,j需要回溯到的地方,这里有一种递归的意味;

再来看i,j变化表,当T[i]处遇到不相等字符时,

若前后缀已匹配的字符数为0,则说明之前是T1[1]和T1[i-1]的比较,即j=1,j值按照j=next[1]=0进行回溯,对应于j==0语句,接下来的三步操作前面已经分析过;

若前后缀已匹配的字符数不为0,如i=7,j=4时,(此时前后缀已匹配的字符数为3),则j值按照j=next[4]=1进行回溯,回溯后比较

T1[7]与T1[1],为什么要回溯到next[4]呢,来看T1字符串:


当i=7时,前面已知前后缀有三个字符匹配,即abc,然后i,j各加1,比较a与d,字符不相等,接下来i值不变,也就是d不变,j值回溯,意即既然长的匹配不了,那就退而求其次,看看短的能不能匹配,这是容易理解的思路,而恰好next[4]中的值指的就是当下标为4的字符与主字符(即i所指的字符)不匹配时,应该与主字符比较的下一个字符的下标,在这里,next[4]为1,则接下来应该比较d与a(a的下标为1)。

下面再举一个例子来说明next数组回溯的妙处:

令字符串T2如下:

T2
9abaeababh
0123456789
get_next程序在求h处的next值的过程为:

已得在下标为8处的b字符处,next[8]=4,意即此时j=4,接下来比较b与e(i=8,j=4),二者不等,

然后,i不变,j值按照j=next[j]=next[4]=2回溯至2,

此时i=8,j=2,比较T2[8]与T2[2],二者相等都是b,至此就不用再回溯了,可得next[9]=j++=3;

这里由于next数组的使用,当b与e不等时,j不用直接跳回到字符串首部让b与a(j=1)去比较,只需要按照next[4]的指引与b(j=2)比较即可,如果相等,可直接得出h处的next值,如果不等,则j需要继续回溯,直至j为0。

事实上,按照next数组的定义,完全可以用朴素的模式匹配算法来求next数组,那样的话就容易理解多了,在上面的get_next函数中,求后面的next值需要用到前面已经求得的next值,正是这一点给算法的理解带来了困难。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值