kmp算法笔记

kmp算法:字符串模式匹配算法,找出子串在母串中的位置。

首先要计算子串中每个字符的模式值(字符首串中最大首尾相同字符的个数),模式值决定了匹配失败的情况下回溯的位置,这样就不用从头开始逐位比较,而是从模式值指定的位置直接开始比较,从而节省了计算时间,这是后续子串与母串匹配进行移位的依据。

如子串字符串“abcabcde”
字符a首串模值-1
字符b首串a模值0
字符c首串ab模值0
字符a首串abc模值0
字符b首串abca模值1
字符c首串abcab模值2
字符d首串abcabc模值3
字符e首串abcabcd模值0
从而得出字符串“abcabcde”的模式为[-1 0 0 0 1 2 3 0]

可以看出,子串模值计算是一个循环比较过程。为了比较首尾字符是否相同,需要设置两个位置变量j,k,k指示首字符位置,j指示尾字符位置,同时j也可以表示循环次数。

很明显,存在特殊情况,k=-1,j=0,此时模值为-1,表示空首串。j>0时,首串非空,且对所有k=-1,j>0得到的模值均为0;

  1. 当k,j处的字符相同时,说明首尾有k+1个字符相同,则j+1处字符的首串模值应为k+1,同时j,k值增加一准备下一次比较;
  2. 当k,j处的字符不同时,k需要根据当前模值进行回溯,而j值保持不变,再次比较是否相等(k值反复回溯,直到j,k处的字符再次相等,从而进入步骤1,若回溯到k=-1,则模值为0)。

模值只有两种变化趋势,字符匹配时递增,字符串不匹配时回溯。

仍然以字符串str=“abcabcde”来观察k,j的变化情况,假设str[-1]=NULL为空,用数组M[]来存储模值。

设置初始情况,k=-1,j=0,M[0]=-1;

1)k=-1,j=0
由于k=-1,故M[1]=0;重置k=0;j++;
2)k=0,j=1
str[0]不等于str[1],回溯到k=-1,故M[2]=0;重置k=0;j++;
3)k=0,j=2
str[0]不等于str[2],回溯到k=-1,故M[3]=0;重置k=0;j++;
4)k=0,j=3
str[0]等于str[3],故M[4]=k+1=1;k++;j++;
5)k=1,j=4
str[1]等于str[4],故M[5]=k+1=2;k++;j++;
6)k=2,j=5
str[2]等于str[5],故M[6]=k+1=3;k++;j++;
7)k=3,j=6
str[3]不等于str[6],回溯到k=M[3]=0,str[0]不等于str[6],回溯到k=M[0]=-1,故M[7]=0;重置k=0;

—k <-----------------
—j 0 1 2 3 4 5 6 7
M -1 0 0 0 1 2 3 0
回溯过程是一个链式结构,直到匹配时或k=-1时结束。
模值表示要回溯的位置,例如当k=3时,3对应的模值为M[3],则回溯值是k=M[3]=0;当k=6时,回溯值是k=M[6]=3,第二次回溯值是k=M[3]=0;

根据上面的思路编写模值数组求解函数如下:

//求出模值,并存放在last数组
//last数组记录了当前匹配不成功时,需要回溯的值
void GetLast(char str[], int len, int last[])
{
	int j, k;				//前位置变量k,后位置变量j
	j = 0; k = -1;
	last[0] = -1;//第一个字符前无字符串,给值-1

	//因为last数组中j最大为len-1,而每一步last数组赋值都是在j++之后
	//所以最后一次经过while循环时j为len-2
	//j处计算的是j+1处的模值,在j+1处进行回溯调用
	while (j < len - 1)
	{
		if (k == -1)		//k=-1时结束匹配
		{
			last[j+1] = 0;	//j+1处模值置为0
			k = 0;			//k值置为0,从头开始
			j++;			//j递增,准备进行下一次匹配
			printf("(1) j=%d,k=%d,last[%d]=%d\n", j, k, j, last[j]);
		}
		else				//k>=0时,继续匹配,或进行k值回溯
		{
			if (str[k] == str[j])			//匹配时
			{
				/*记录模值大小,这里因为k是数组下标,所以需要k + 1来得到实际模值的大小,模值同时对应着j+1处要回溯的k的位置*/
				last[j+1] = k+1;				
				k++;				//k值递增
				j++;				//j值递增
				/*准备进行下一次匹配*/
				printf("(1) j=%d,k=%d,last[%d]=%d\n", j, k, j, last[j]);
			}
			else				//不匹配时,回溯k
			{
				k = last[k];	//根据当前last数组模值得到回溯的k
			}
		}
		
	}
}

程序可以进一步简化成如下:

//求出模值,并存放在last数组
//last数组记录了当前匹配不成功时,需要回溯的值
void GetLast(char str[], int len, int last[])
{
	int j, k;				//前位置变量k,后位置变量j
	j = 0; k = -1;
	last[0] = -1;//第一个字符前无字符串,给值-1

	//因为last数组中j最大为len-1,而每一步last数组赋值都是在j++之后
	//所以最后一次经过while循环时j为len-2
	//j处计算的是j+1处的模值,在j+1处进行回溯调用
	while (j < len - 1)
	{
		if (k == -1 || str[k] == str[j])			//k=-1,或者匹配时
		{
								
			k++;				//k值递增
			j++;				//j值递增
			last[j] = k;		//记录模值大小
			/*准备进行下一次匹配*/
			printf("(1) j=%d,k=%d,last[%d]=%d\n", j, k, j, last[j]);
		}
		else				//不匹配时,回溯k
		{
			k = last[k];	//根据当前last数组模值得到回溯的k
		}
	}
}

这里有需要改进的地方:如果回溯前的字符和回溯后的字符是相同的,那这次回溯是无效的,就需要再往前回溯一次,直到出现不同的字符。我们需要一个新的数组last2来存储新的回溯值,来保证每一次通过last2回溯到的都是不同于当前的字符。
构造last2是一个迭代的过程,需要在匹配过程中增加一个判断。
在k,j处,判断k+1和j+1处的匹配情况,当字符不匹配时,从j+1处回溯到k+1处一定是不同的字符,即last2[j+1]=k+1;当字符匹配时,即j+1与k+1处有相同字符,则j+1先回溯到k+1,再从k+1处通过last2往前回溯一次即可得到不同字符,由于k+1处的last2回溯值已经计算完成,最终j+1处的回溯值确定为last2[j+1]=last2[k+1]。

代码如下:

//求出模值,并存放在last数组
//last数组记录了当前匹配不成功时,需要回溯的值
void GetLast2(char str[], int len, int last2[])
{
	int j, k;				//前位置变量k,后位置变量j
	j = 0; k = -1;
	last2[0] = -1;//第一个字符前无字符串,给值-1

	//因为last数组中j最大为len-1,而每一步last数组赋值都是在j++之后
	//所以最后一次经过while循环时j为len-2
	//j处计算的是j+1处的模值,在j+1处进行回溯调用
	while (j < len - 1)
	{
		if (k == -1 || str[k] == str[j])			//k=-1,或者匹配时
		{
								
			k++;				//k值递增
			j++;				//j值递增
			if (str[k] != str[j])		//下一位不匹配时
			{
				last2[j] = k;			//模值(回溯值)大小确定
			 }
			else						//下一位匹配时
			{
				last2[j] = last2[k];		//模值(回溯值)修正,将前面计算好的last2[k]赋给后面的last2[j]				
			}
			
			/*准备进行下一次匹配*/
			printf("(1) j=%d,k=%d,last2[%d]=%d\n", j, k, j, last2[j]);
		}
		else				//不匹配时,回溯k
		{
			k = last2[k];	//根据当前last数组模值得到回溯的k
		}
	}
}

完成模式数组的计算后,子串与母串的匹配也是一样的原理。对母串与子串进行遍历,每当无法回溯或者字符匹配时,就进入下一轮匹配,否则子串就回溯直到无法回溯或者再次匹配。若子串遍历完成则返回索引值。

/*KMP算法
 *str1:母串,len1:母串长度,str2:子串,len2:子串长度
 * 先计算子串模式数组,然后进行母串与子串的比较与回溯
 */
int KMP(char str1[], int len1, char str2[], int len2)
{
	int i = 0, j = 0;				//i:母串位置变量;j:子串位置变量
	int last2[MaxSize];				//子串模式数组

	GetLast2(str2, len2, last2);	//计算模式数组
	while (i < len1 && j < len2)				//未超出范围
	{
		if (j == -1 || str1[i] == str2[j])		//无法回溯,或者字符匹配
		{
			i++;	//位置递增
			j++;	//位置递增
		}
		else					//不匹配
		{
			j = last2[j];		//进行回溯
		}	
	}
	/*当i或者j超出范围,即母串或者子串完成遍历*/
	if (j >= len2)			//子串已经完成遍历
	{
		return i - len2;	//计算子串在母串中的索引位置
	}
	else					//子串未完成遍历
		return -1;
}

主函数代码如下:

int main()
{
	char str1[] = "aaaaabaaaaac";
	int len1 = strlen(str1);
	char str2[] = "aaaaac";
	int len2 = strlen(str2);
	int index = -1;

	cout << "str1:" << str1 << endl;
	cout << "str2:" << str2 << endl;
	index = KMP(str1, len1, str2, len2);
	cout << "index=" << index << endl;

	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chvngzhvng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值