从朴素匹配算法到KMP算法

一、串的定义

#define MAXLEN 255

typedef struct {
	char ch[MAXLEN];
	int length;
}SString;

定义结构体SString,结构体成员分别为:

1.char型字符;

2.int型串长length。


二、串的朴素模式匹配算法(BF算法)

1.代码实现

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

2.代码解释

(1)形参SString SSString T分别为子串与模式串;

(2)定义int i指向子串的对应位置;int j指向模式串的对应位置;

(3)while循环:

        ①循环条件:i <= S.length && j <= T.length;即i和j的指向都没有超出主串和模式串时;

        ②if条件:若S.ch[i] == T.ch[i]即主串和子串的字符相等时,对i和j都自增1;

           若不满足if条件时,i=i-j+2(指向主串下一个字符);j=1(回溯到第1个字符重新对比);

第一次循环不满足if条件,i指向下一个A,j继续指第1个L:

 第二次循环不满足if条件,i指向下一个M,j继续指向第一个L:

  第三次循环不满足if条件,i指向下一个L,j继续指向第一个L:

 第四次循环满足if条件,i、j自增1:

 第五次循环满足if条件,i、j自增1:

 第六次循环满足if条件,i、j自增1:

 下一次不满足循环条件,退出while循环,此时j=4>T.length,返回i - T.length=4。

(4)如果遍历完主串都没有匹配到模式串,退出循环时应该是i>S.length,j=1;此时j不大于T.length,返回0;

(5)存在一种特殊情况:上面例子中主串S如果为 I A M L O,当L O匹配后,i=6,j=2;此时i>S.length退出while循环,但j<T.length,返回0。


朴素模式匹配算法存在的缺点是:

1.若主串长度为n,模式串长度为m,则时间复杂度O(mn),较高;

2.朴素模式匹配算法每次不匹配时,都会将i回溯到紧挨着的下一个对比子串中,如果碰到极端例子,会经常性地对指针进行回溯,浪费大量时间。

故提出对BF算法的优化——KMP算法:


三、KMP算法

1.优化思路

主串指针不回溯,只有模式串指针回溯;

2.举例说明

假设模式串为abcabd,分以下几种情况讨论:

①若当前字符匹配的情况下,则i++;j++;

②若j=1时发生不匹配,也就是第一个字符a没有匹配,则i++;j应该回溯到1,开始重新找a进行匹配;但这时的j=1不是直接回溯到1,而是回溯到j=0然后让i和j都自增1;

 

③若j=2时发生不匹配,也就是第二个字符b没有匹配,则j应该回溯到1重新开始找a进行匹配;

 

④若j=3时发生不匹配,也就是第三个字符c没有匹配,则j应该回溯到1重新开始找a进行匹配;

 

⑤若j=4时发生不匹配,也就是第四个元素a没有匹配,则j应该回溯到1重新开始找第一个a进行匹配;

 

(至此临时解释一下为什么以上情况需要回溯到开头重新找:因为abc这三个元素没有重复的,至少后面几个元素(bc)不是第一个元素a,如果有和开头元素或者开头几个元素重复的,那就可以少回溯几个位置,从重复的元素后开始找,比如下面这些情况:)

⑥若j=5时发生不匹配,也就是第五个元素b没有匹配,这时可以不用从头开始回溯了,解释如下:

当第五个元素没有匹配,也就是说前四个元素abca已经匹配了,那abca里出现了第四个元素a和第一个元素a相同,可以直接跳过第一个a开始找,即j=2,期待下一个i是不是b就可以了。这时少回溯1个位置,节省了时间;

 

⑦若j=6时发生不匹配,也就是第六个元素d没有匹配,则可知前五个abcab已经匹配,发现后面的ab和开头的ab相同,则可以跳过前两个ab,即j=3,期待下一个i是不是c,这时少回溯2个位置,节省时间;

 

⑧若全部匹配则匹配成功。

3.代码实现:

int Index_KMP1(SString S, SString T, int next[])
{
	int i = 1, j = 1;
	while (i <= S.length && j <= T.length)
	{
		if (j = 0 || S.ch[i] == T.ch[i])		//j=0的条件适用于第一个元素不匹配
		{
			++i;
			++j;
		}
		else
			j = next[j];						//调用next数组对应元素
	}
	if (j > T.length)
		return i - T.length;
	else
		return 0;
}

4.补充解释

(1)⑥⑦两种情况中所提到的跳过a、跳过ab直接往后找,是因为我们找到了a、ab和前面重复。给出以下两个术语:

①前缀:包含第一个字符,且不包含最后一个字符的子串;

②后缀:包含最后一个字符,且不包含第一个字符的子串;

也就是我们在找重复元素时,实际上是找了某一个子串中相同的前缀和后缀,那这个子串是谁呢?

⑥⑦两种情况中,我们分别在abca和abcab中搜索了相同的前后缀,所以应该是匹配失败前的所有字符所组成的串。

(2)j=1不匹配,回溯j=0;(特殊情况,实际上是回溯到j=1)

         j=2不匹配,回溯j=1;

         j=3不匹配,回溯j=1;

         j=4不匹配,回溯j=1;

         j=5不匹配,回溯j=2;

         j=6不匹配,回溯j=3;

(3)这种回溯方式我们可以定义一个int next[]数组来存放这些回溯信息,当j=k且发现字符不匹配时,令j=next[k],基于上面的分析,给出以下求next数组的方法:当第j个字符匹配失败时,由前1~j - 1个字符组成的串记为S,则next[j] = S的最长相等的前后缀长度 + 1;

(4)KMP算法时间复杂度O(m+n)


四、next、nextval数组

1.next数组

(1)至此我们已经知道KMP算法的核心关键就在于求出next数组,给出手算和机算两种方法:

①手算:当第j个字符匹配失败时,把前面1~j-1个字符提取出来,找到相同的前后缀,记其长度为k(若没有相同的前后缀则长度k=0),则next[j]=k+1;

②机算:

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

(2)结合给出的求next数组函数我们可以将KMP算法改进为:

int Index_KMP2(SString S, SString T)
{
	int i = 1, j = 1;
	int next[T.length + 1];
	get_next(T, next);
	while (i <= S.length && j <= T.length)
	{
		if (j == 0 || S.ch[i] == T.ch[i])
		{
			++i;
			++j;
		}
		else
		{
			j = next[j];
		}
	}
	if (j > T.length)
		return i - T.length;
	else
		return 0;
}

2.nextval数组

(1)nextval数组是对next数组的优化,实际上next数组存在一种特殊情况下的问题,举个例子:

(2)之前的例子中,a b c a b d 所求的next数组为 0 1 1 1 2 3;

        ①按原来的方法,第四个字符a不匹配时,将j回溯到j=1,开始比较主串与模式串第一个字符a是否相等,但我们已知主串当时这个字符已经和a不匹配了,所以再比较一次就没有意义了;

        ②故将next[4]不再赋予1而赋予0,也就是和next[1]相等,此时i++;j++;跳过i对第一个字符a的匹配进行下一个i对a的匹配;

        ③当第五个字符b不匹配时,原本j=2,开始比较主串和b是否相等,但已知和b不匹配,比较失去意义;现在将j=next[2]=1,回溯j=1,比较i对应的字符和模式串第一个字符a是否相等;

        ④优化后的next数组为 0 1 1 0 1 3。

(3)nextval数组的求法:

        ①先算出next数组;
        ②令nextval[1]=0;
        ③


for (int j = 2; j <= T.length; j++)
{
	if (T.ch[next[j]] == T.ch[j])
		nextval[j] = nextval[next[j]];
	else
		nextval[j] = next[j];
}

(4)用nextval数组替代next数组即可提高效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值