串的模式匹配之KMP算法

想理一理KMP算法的学习思路,但是不会制作动图表达,暂时就用文字描述,尽量表达清晰吧。
注:看多了各种解说会发现,有些算法讲解和代码实现有出入,主要是因为针对的字符串存储方式不一样,要先明确串的值从0还是1单元开始存放。

模式匹配

给定主串s和子串t,则在s中找到t的过程称为模式匹配,其中t称为模式。如果在s中找到等于t的子串,则匹配成功,返回t在s中首次出现的存储位置,否则匹配失败,返回-1。
为了运算方便和保持思路清晰,设字符串的长度存放在0号单元,串的值从1号单元开始存放,这样字符序号和存储位置一致。

朴素匹配算法(暴力匹配)

基本思想:首先将s[1]和t[1]进行比较,若不同,则s[2]与t[1]比较(主串向后移动一位)…直到s中的s[i]与t[1]相同,再将它们之后的字符相比较,若下一个也相同,则如此继续往下比较,当s[i]与t[j]不相同时,则主串s回到本趟开始字符的下一个字符,即s[i-j+2],子串则返回到t[1],继续开始下一趟的比较,重复上述的过程。若t中的字符全部比完,则说明此趟匹配完成,起始位置是i-j+1,或者i-t[0],否则匹配失败。
例如s=“5abcba”, t=“2bc”,则返回2,含义是主串从第2位字符开始匹配出模式串。

此思想十分自然,根据此法,算法描述为:

int StrIndex_BF(char *s,char *t)
{
	int i = 1,j = 1;                    //从主串s的第一个字符开始比较,s[0]和t[0]存储的是字符串长度
	while(i <= s[0] && j <= t[0])
	{
		if(s[i] == t[j]) {i++;j++}      //相等则均后移一位进行下一步比较
		else {i = i-j+1; j=1;}          //出现不匹配情况,则回溯,主串回到此趟开始位的下一个字符,子串回到第一个字符
	}
	if(j > t[0]) return(i - t[0]);       //匹配完成,返回存储位置
	else return -1;                      //否则返回-1
}

此算法的时间复杂度为(O(n+m) ~ O(n*m),其中n,m分别是两个串长度。

KMP算法

暴力匹配算法简单易理解,但是效率很低,在此基础上,发展生成了KMP算法,其中KMP名称是取自三个发现人的名字首字母。
原始算法的效率低在于非必要的回溯,在出现不匹配字符时,该字符之前的字符元素排序若出现重复(前后缀相同),则可以利用此性质,舍去重复部分的比较,直接滑到重复后一位比较,此时主串的比较位(指针)不回溯,保持不变,将模式串的比较位(指针)移动到重复部分的后一位,与主串的比较位开始进行下一轮比较。
具体描述:若s[i]与t[j]不相同,若此时模式串t前k-1个字符t[j]之前的k-1个字符相同,则将模式串指针移到t[k],保持主串比较位不变,此时就是从该**第k个字符与s[i]**进行比较,以此类推。

此思想实现的重点就在于这个模式串指针移动到的位置,此时就引出了著名的next[j]数组,在t[j]上出现不匹配情况时,就将next[j]值对应的第next[j]个字符拿来进行下一轮比较。此处next[j]的获取后续再说,假设数组已知,在此基础上,代码实现算法为:

int StrIndex_KMP(char *s,char*t,int pos)            //从主串s第pos个字符开始比较,在暴力匹配中pos=1
{
	int i = pos,j =1,slen,tlen;
	while(i <= s[0]&& j <= t[0])
	{
		if(s[i] == t[j]) {i++;j++}
		else j = next[j];                          //出现不匹配字符,则模式串比较位回溯
	}
	if(j > t[0]) return(i - t[0]);                  //匹配完模式串元素,则匹配成功,返回存储位置
	else return -1;                                 //否则匹配失败
}

next[]求解(关键)

     next[]是关于模式串的指针移动位置的数组,只与模式串t的模式(字符元素)有关。

前后缀概念

是的,在此之前先了解一波()前后缀知识,一个字符串,前缀是首字符开始的不包含尾字符的子串,相应后缀为不包含首字符以尾字符结束的子串。
例如:字符串s=“abcab”,前缀有a,ab,abc,abca,后缀有bcab,cab,ab,b。
接下来就是提出前后缀重叠概念,对于一个字符串,若存在其某前缀与某后缀相同,则描述为前后缀重叠,重叠位数出现最多的前后缀,就是最大前后缀重叠,求解next[]就是发现每一个子字符串的最大前后缀重叠数。
在上诉例子中,前缀ab和后缀ab就出现了重叠,且唯一,最大重叠是2。

next[]求解:
实例说明,例如字符串t=“ababaaab”,以数组形似存储,T[0] =8, T = {8,a,b,a,b,a,a,a,b},其中T[0]即为上诉所说表示字符串长度,这样一来,下标和字符所在位数一致。

下标 j 0 1 2 3 4 5 6 7 8
T {8, a, b, a, b, a, a, a, b}
next[j] {\ 0 1 1 2 3 4 2 2}

令next[1] = 0, 意味着T[1] = a出现不匹配时,则将T[0]与主串当前位开始下一轮比较(后移一位)
next[2] = 1,意味着T[2] = b 出现不匹配时,则将T[1]与主串当前位开始下一轮比较(后移一位)
j = 3,T[3] = a 出现不匹配时,观察T的前2个元素组成的字符串的最大前后缀重叠,即“ab”的前后缀重叠,此时无重叠,则next[3] = 0+1 = 1;
j = 4,T[j] = b出现不匹配时,观察T的前3个元素组成的字符串的最大前后缀重叠,即“aba”的前后缀重叠,此时重叠前后缀为“a”,则next[4] = 1+1 = 2;
j = 5,T[j] = a出现不匹配时,观察T的前4个元素组成的字符串的最大前后缀重叠,即“abab”的前后缀重叠,此时重叠前后缀为“ab”,则next[5] = 2+1 = 3;
j = 6,T[j] = a出现不匹配时,观察T的前5个元素组成的字符串的最大前后缀重叠,即“ababa”的前后缀重叠,此时重叠前后缀为“aba”,则next[6] = 3+1 = 4;
j = 7,T[j] = a出现不匹配时,观察T的前6个元素组成的字符串的最大前后缀重叠,即“ababaa”的前后缀重叠,此时重叠前后缀为“a”,则next[7] = 1+1 = 2;
j = 8,T[j] = b出现不匹配时,观察T的前7个元素组成的字符串的最大前后缀重叠,即“ababaaa”的前后缀重叠,此时重叠前后缀为“a”,则next[7] = 1+1 = 2;

代码实现

void get_next(char *t, int * next)
{
	int j = 0, i = 1;           //前缀和后缀比较位
	next[1] = 0;
	while(i < t[0])
	{
		if(j == 0 || t[i] == t[j]) {i++;j++; next[i] = j;}   //前后缀相同则继续下一位
		else j = next[j];
	}
}

优化:
在上诉求解过程中,计算前 j-1 个字符构成的字符串的前后缀重叠时,是没有考虑
*t[j]**的,但是想想一下,如果最大相等前缀的后一个字符与现在不匹配的 t[j] 是相等的话,那我们按照前述方法移动,与主串当位置比较的元素也是没有变化的,即肯定也是不匹配的,则此次移动匹配非必须。

实例说明:
上诉 j = 5 ,T[j] = a出现不匹配时,观察T的前4个元素组成的字符串的最大前后缀重叠,即“abab”的前后缀重叠,此时重叠前后缀为“ab”,则next[5] = 2+1 = 3,即将模式串右移2位,但是此时相同前缀“ab” 的后一个字符a 等于此时的T[j],也就是说我们把模式串右移两位后,比较位并没有发生任何改变,肯定依旧不匹配,根据此思想,我们完全可以将T[j] 的值考虑进去,避免这种肯定无效的移动。

代码实现:

void get_next(char *t, int * next)
{
	int j = 0, i = 1;           //前缀和后缀比较位
	next[1] = 0;
	while(i < t[0])
	{
		if(j == 0 || t[i] == t[j]) 
		 	{i++;j++; 
			if(t[i] == t[j]) next[i] = next[j]; //前缀后一位与当前t[j]相等,则跳转到前缀j对应地next位
			else 
			  	next[i] = j;    //前缀后一位与当前t[j]不相等,则按照原方法标记
			}   
		else j = next[j];
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值