四种next数组及KMP算法在不同next数组下的解释


前言

文章介绍不同next数组的区别以及相应的KMP算法的区别,并解释一下生成next数组的原理。

#KMP算法是什么:KMP算法是字符串匹配中比BF算法(朴素的搜索算法)更加快速的算法。KMP算法中指向主串的i指针(这里的指针不是指内存地址,是遍历算法中常用的伪指针)永不后退,而子串中的 j 指针在失配时不会每次都退回首元素的位置,而会根据next数组跳过一些没必要匹配的元素。

BF算法

int Index(String S, String T, int pos)
{
	int i = pos;
	int j = 1;
	while(i <= S.Len && j <= T. Len)
	{
		if(S.ch[i] == T.ch[j])  //对成功匹配的处理
		{
			i++;
			j++;
		}
		else   //对失配的处理
		{
			i = i-j+2;//i退回上次匹配首位的下一位
			j = 1;    //j退回子串T的首位
		}
	}
	if(j > T.Len)
		return i-T.Len;//匹配成功
	else
		return 0;

KMP算法(第三种next数组)

int Index_KMP(String S, String T, int pos)
{
	int i = pos;
	int j = 1;
	while(i <= S.Len && j <= T. Len)
	{
		if(S.ch[i] == T.ch[j])  //对成功匹配的处理
		{
			i++;
			j++;
		}
		else   //对失配的处理
		{
			j = next[j];   //i不用回溯,j根据next数组回溯
		}
	}
	if(j > T.Len)
		return i-T.Len;//匹配成功
	else
		return 0;

可以看出来,KMP算法只是在失配的时候修改一下操作。使得 i 不再无脑回退,j 也不再无脑回退到子串的首位,而变成根据next数组跳过一些多余的匹配过程。

一、四种next数组的区别

第一种(对应模式串索引从0开始)
012345
aabaaf
010120
前缀表,以这种为基准可以衍生出以下三种。这种缺点是首元素和后续的元素无法区分开,所以在后续KMP算法中要增加是否为首元素的判断,代码实现略微复杂,但可忽略不计。


第二种(对应模式串索引从0开始)
012345
aabaaf
-10-101-1
前缀表整体减一,后续使用时再整体加一。


第三种(对应模式串索引从0开始)
012345
aabaaf
-101012
整体右移优化后的next数组,将首元素设为0,其他元素前面无相等前后缀的设为1,整体右移是为了区别开首元素,这样后续代码实现方便。


第四种(对应模式串索引从1开始)
123456
aabaaf
012123
整体右移优化后又整体加一的next数组,将首元素设为0,其他元素前面无相等前后缀的设为1,整体右移是为了区别开首元素,这样后续代码实现方便。整体加1是为了令模式串从索引1开始,因为串的首字符用于记录串的长度。

关于整体右移优化的影响:
前缀表,也就是第一种next数组的使用:

int Index_KMP(String S, String T, int pos, int next[])
{
	i = start; //主串 
	j = 0;     //子串 
	while(i < S.length && j < T.length)
	{
		if( S.str[i] == T.str[j]) //字符匹配 
		{
			i++;
 			j++;
		}
		else if(j > 0) 
			j = next[j-1]; //根据next数组跳过一些不必要的匹配 
		else  //比较第一个字符时失配(j == 0)
			i++;
	}
	if(j == T.length)
		return i-j;
	else
		return -1;
}




整体右移也就是第三种next数组的使用:

int Index_KMP(String S, String T, int start, int next[])
{
	i = pos; //主串 
	j = 0;     //子串 
	while(i < S.length && j < T.length)
	{
		if(j == -1 ||  S.str[i] == T.str[j]) //相等前后缀长度为0或字符匹配
		{
			i++;
			j++;
		}
		else if(j > -1)//这个if可以去掉
			j = next[j]; //根据next数组跳过一些不必要的匹配 
		//else //比较第一个字符时失配(j == 0)
		//{
		//	i++;
		//	j++;
		//} 
	}
	if(j == T.length)
		return i-j;
	else
		return -1;
}

注意到右移后发生了一个语句变化: j = next[j-1] 变成了 j = next[j]
我们可以把next[ ]看成一个取对应元素前面的最长相等前后缀长度的函数(后面会有图解释)。因为next本身存储的就是最长相等前后缀长度。对元素f,取它最长相等前后缀长度在前缀表中就是next[5-1]也就是2,在右移后就是next[5]也是2。

索引012345
模式串aabaaf
前缀表010120
右移后-101012

右移后还可以将比较第一个字符时失配的情况与匹配成功的情况合并 。因为匹配成功后是 i 和 j 都要+1来匹配下一个字符。而第一个字符失配只说明 i 必须+1。如果要合并的话就应该消除 j++;的影响。所以右移后next数组第一位减一。



整体右移且整体加一也就是第四种next数组的使用:

int Index_KMP(String S, String T, int start, int next[])
{
	i = start; //主串 
	j = 1;     //子串 
	while(i < S->length && j < T.length)
	{
		if( j == 0 || S->str[i] == T.str[j]) //相等前后缀长度为0或字符匹配
		{
			i++;
			j++;
		}
		else  
			j = next[j]; //根据next数组跳过一些不必要的匹配 
	}
	if(j > T.length)
		return i-T.length+1;
	else
		return 0;
}

整体加1后,因为是对索引从1开始的字符串的匹配,所以对于主串和子串的匹配都应该从1开始,所以start不能为0 且 j = 1。并且最后返回位置的时候也应该+1。



二、生成next数组算法的难点剖析

int get_next(String T, int next[])
{
	/*next数组,next[0] = -1,子串索引从0开始*/
	int i = 0;
	int j = -1;
	next[0] = -1;
	while(i < T.length)
	{
		if (j == -1 || T.str[i] == T.str[j])
		{
			i++;
			j++;
			next[i] = j;
		}
		else
			j = next[j];
	}
}

这里的 i 和 j 和简单比较用的双指针不一样。i 指向的是后缀的末尾,j指向的是前缀的末尾。同时前缀前面是没有字符的,所以可以将 j 的值理解为最长相等前后缀的长度

学习KMP最难的就是理解生成next数组时的回退:j= next[j]。在这里我们可以把next[ ]看成一个函数,功能为取某字符前面的最长相等前后缀的长度。这时取得的next[j]作为下标刚好就跳过了前缀。说起来有点复杂,让我们用图来解释:

索引012345678
模式串afacafafe
右移后-10010123?

好,这时我们需要求next[8]。所以此时 i = 7,由图可知 j = 3。

可知 T.str[i] != T.str[j],即c != f(为什么是 T.str[i] 和 T.str[j] 比较?因为 j 是最长相等前后缀的前缀末。如果相等那么说明最长相等前后缀长度可以+1。如果是 j == -1时呢?这时候其实长度也是0,只是让 j-1 可以抵消 j++;的影响,实际上,j 变成-1就立马 j++;了)如果是等于则最长相等前后缀从abc变成abaf。j = 4;next[8] = j;既然不相等,那么执行语句:j = next[j]后比较。如果还不相等则继续执行语句:j = next[j]后继续比较。其实这就是一种递归:j      next[j]      next[ next[j] ]

next[j]长这样:

在这里插入图片描述
next[ next[j] ]长这样:
在这里插入图片描述

所以,next[j]实际上就是取j前面字符的最大相等前后缀的长度

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值