KMP算法中Next数组和Nextval数组的手工求解与代码实现

1.串的模式匹配算法

子串的定位操作通常称为串的模式匹配,是各种串处理系统中最重要的操作之一。

最基本的算法就是暴力匹配法。
即从主串的第一个字符开始和子串第一个字符挨个比较。若中途匹配失败,则从主串的第二个字符开始和子串的第一个字符挨个比较。若匹配失败,则从主串的第三个字符开始和子串的第一个字符挨个比较。若匹配失败,……

下面的程序是从主串S的第pos个字符开始和子串T匹配。返回匹配成功后,子串在主串中的位置,若不存在则返回0。

int Index1(SString S, SString T, int pos)
{
	int i, j = 1;
	if (pos >= 1 && pos <= S[0]) {
		i = pos;
		while (i <= S[0] && j <= T[0]) {
			if (S[i] == T[j]) {
				++i;
				++j;
			}
			else {
				i = i - j + 2;
				j = 1;
			}
		}
		if (j == T[0] + 1)
			return i - T[0];
		else
			return 0;
	}
}

此算法的效率非常低,最坏的情况下,时间复杂度为O(n*m)。
比如子串为‘000000001’,主串为‘000000000000000000000000000000000000001’。

2.KMP算法

KMP算法的时间复杂度为O(n+m)。
例:(经典匹配步骤)
KMP算法中Next数组和Nextval数组的手工求解与代码实现_第1张图片

本质是:每次发现字符不匹配后,主串的 i 不需要回溯(第二次比较根本没必要,应该直接进行第三次比较),而是利用已经得到的部分匹配结果将模式串(即子串)向右滑动尽可能远的一段距离后再进行比较。

在上例中,i 代表主串的下标,j代表子串的下标。
第一次匹配失败时,i = 3,j = 3。
按照KMP算法,此时 i 不回溯(即 i 还是等于3),模式串应该向右滑动多少距离,即主串的第3个元素应该和子串的某一元素开始比较。与哪个元素开始比较?取决于模式串向右滑动的距离(j 的值要变为多少?)。
在上例中可以看出,当第一次匹配失败时,模式串向右滑动,j 的值应该变为 1。然后再次比较。

引出next[]数组
当匹配失败时,j 要变为的值只与模式串和当前的 j 值相关,与主串和 i 无关。(要变为的值是一个与 i 相关的函数)。所以,先根据子串本身,求出当 j=1、j=2、j=3……时的函数值。然后放入next[]数组中,当匹配失败时,需要滑动,j 值需要改变,则直接令 j = next[ j ]。

假设已经根据子串求出next[]数组。
则采用KMP算法的模式匹配程序为

int Index_KMP(SString S, SString T, int pos, int next[])
{
	if (pos < 1 || pos > S[0])
		return ERROR;

	int i = pos;
	int j = 1;
	while (i <= S[0] && j <= T[0]) {
		if (j == 0 || S[i] == T[j]) {
			++i;
			++j;
		}
		else 
			// i = i - j + 2;  //i 不再回溯
			// j = 1;			//仅此处两行和经典算法不同
			j = next[j];		//j 
		
	}
	if (j > T[0])
		return i - T[0];
	else
		return 0;
}

上面的程序仅当匹配失败时的处理和经典算法不同。

3.Next数组求解

先看一下next数组的公式:
KMP算法中Next数组和Nextval数组的手工求解与代码实现_第2张图片
手工求法:

  1. 第一二位对应的next值分别为0和1
  2. 后面每一位的next值求解:根据前一位进行比较
    - 将前一位的字符 与前一位的next值作为下标对应的字符进行比较
    - 相等,则该位的next值就是前一位的next值加上1
    - 不等,向前继续寻找next值对应的内容来与前一位进行比较,直到找到某个位上内容的next值对应的内容与前一位相等为止,则这个位对应的值加上1即为需求的next值
    - 若找到第一位都不匹配,则改为的next值为1。

KMP算法中Next数组和Nextval数组的手工求解与代码实现_第3张图片
根据手工求法的步骤和例子,用代码实现求next[ ]数组。
原创代码:

void get_next(SString T, int next[])
{
	next[1] = 0;
	next[2] = 1;
	
	int i = 3, j = 0, k = 0;
	while (i <= T[0]) {
		j = next[i - 1];
		if (T[j] == T[i - 1])		// 将前一位的**字符** 与前一位的next值作为下标对应的**字符**进行比较 
			next[i] = next[i - 1] + 1;		//相等
		else {		//不等,继续向前找
			k = j;
			j = next[j];
			if (j == 0)	
				next[i] = 1;
				while (j >= 1) {
				if (T[j] == T[i - 1]) {
					next[i] = next[k] + 1;
					break;
				}
				k = j;
				j = next[j];
			}
			if(j == 0)
				next[i] = 1;
		}
		++i;
	}			

以下是上面的代码优化后(书上的):

void get_next(SString T, int next[])
{
	int i = 1, j = 0;
	next[1] = 0;
	while (i < T[0]){
		if (j == 0 || T[i] == T[j]) {
			++i;
			++j;
			next[i] = j;
		}
		else
			j = next[j];
	}
}

4.Nextval数组求解(总结:同为0,不同为1)

Nextval 数组是基于next数组求出来的。next数组再某些情况下回有不必要的匹配,nextval是对next数组的优化。

KMP算法中Next数组和Nextval数组的手工求解与代码实现_第4张图片
具体步骤:若前两位相同,则nextval为0,若不同则为1,
第二位的b与第一位的a不同,所以为1
第三位的时候next值为1,第三位的a与当前next值所对应的值(即是第一位的a)相同, 所以为0。
第四位的next值为2,2对应的值为b,与当前的值不相同,nextval值为当前的next值,第四位为2
第五位的next值为2,2对应的值为b,相同,继续比,第二位b对应的next值为1,1对应的next值 为a,不同,结束,所以nextval值为1。
第六位的next值为3,3对应的值为a,不同,则nextval为next的值为3.
第七位next值为1,1对应的值为a,相同,已经是第一位,不再继续往前了。nextval则为0.
第八位的next值为2,2对应的值为b,不相同则为next的值,为2.

规律
不同则为next值。
相同则继续往前比较,直到不同或第一位。最终则为0,不同则为对应next值。

程序如下:

void get_nextval(SString T, int nextval[])
{
	int i = 1, j = 0;
	nextval[1] = 0;
	while (i < T[0]) {
		if (j == 0 || T[i] == T[j]) {
			++i;
			++j;
			if (T[i] != T[j])
				nextval[i] = j;
			else
				nextval[i] = nextval[j];
		}
		else
			j = nextval[j];	
	}
}

你可能感兴趣的:(数据结构与算法)