朴素模式匹配算法和KMP模式匹配算法

在讲这两个算法之前,需要先了解什么是模式匹配?
模式匹配的定义:
  设有主串S和子串T,子串T定位是指在主串S中找到一个与子串T相等的子串。通常把主串S称为目标串,把子串t称为模式串,因此定位也称为模式匹配。匹配成功,是指在目标串S中有一个子串等于模式串T;匹配失败,是指目标串S不存在子串等于模式串T。
举一个简单一点的例子吧,在一篇文章中,你想要找到一句话在文章中是否出现,如果出现,就是匹配成功,如果没有出现就表示匹配失败!

朴素的模式匹配算法

朴素的模式匹配算法比较好理解,朴素的模式匹配算法是将字串与主串一一进行比较。
在这里插入图片描述上述图片就表示了朴素的模式匹配算法的过程。从主串的第一个元素开始对比,若不与子串相同,则向后移动一位继续对比,一直对比到模式串的最后一位于主串最后一位对齐,若中间没有与子串匹配,则匹配失败,若有与之匹配的,则匹配成功,返回匹配的位置。
接下来我们来看代码:

/*返回子串T在主串中的位置,若不存在,则返回0.*/
/*T[0] 和 S[0] 表示字符串的长度*/
int Index(string T,string S)
{
	int i = 1;	//i表示主串S的下标
	int j = 1;	//j表示子串T的下标
	while(i <= s[0] && j <= t[0]){	//若i小于S长度且j小于T的长度进行循环 
		if(S[i] == T[j]){				//两个字符若相等,则继续 
			++i;
			++j;
		}
		else{							//若不想等,则回退,i退为上一次的下一个,j退为1
								
			i = i - j + 2;
			j = 1;
			 
		}
	}
	if(j > T[0])	return i - T[0];	 
	else 			return 0;
}

这就是朴素的模式匹配算法的代码。
这虽然能够找出主串中与子串相匹配的,但它的时间复杂度是非常高的,举一个例子:
若主串为S = 000000000000000000000000000001
字串为T = 000001
这个例子若用上述方法,它就需要主串前面24个元素每一个都与子串中的元素相比较,非常的浪费时间。

KMP模式匹配算法

继续来看这个例子:
在这里插入图片描述在匹配第一次之后,我们发现,子串中第一位g与主串中后面的几位均不相同,则可以直接跳到第5步执行。如此,我们的步骤将会非常简单,我们也省略了许多不必要的步骤。
我们来再看一个例子:
在这里插入图片描述在这个例子中,我们也发现2、3、4、5步都是可以省略的,所以我们可以只进行第1、6步。
接着来看下一个例子:
在这里插入图片描述在这个例子中,我们发现,第二三步就可以省略。我们可以直接进行第四步。通过上面两个例子发现,我们要省略的步骤其实与主串没关系,和子串前后缀的相似程度有关系,所以我们把子串拿出来单独研究。
在abcdex这个例子中,当 j 等于6时,x与主串不匹配,j 变为了1,在abcabx这个例子中,我们的x与主串不匹配,j 从6变为了3。
我们把子串各个位置的 j 值变化定义为一个数组,将其称为next数组。
在这里插入图片描述来看一下这个例子:
在这里插入图片描述再来看这个例子:
在这里插入图片描述

3.T=“ababaaaba”
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在上述例子中我们已经大致了解了next数组的推导方式,接下来我们来看一下next数组的代码实现:

/*
	next 数组
	
	T[0] 表示子串的长度 
*/ 
void get_next(string T,int *next)
{
	int i ,j;
	i = 1;
	j = 0;
	next[1] = 0;
	while(i < T[0]){
		
		if(!j || T[i] == T[j]){		//T[i] 表示后缀单个字符 
			++i;						//T[j] 表示前缀单个字符 
			++j;
			next[i] = j;
		}
		else{
			j = next[j];				//若字符不同,则j 回溯 
		}
		 
	}	
}

以上是next数组推导方式的代码。
讲了这么多,什么是KMP算法呢?
KMP算法就是避免重复遍历的一种算法。
接下来我们将要用next数组来实现我们更加简便的算法了。

//返回子串T在主串中的位置,若不存在,返回0
//T非空 
int Index_KMP(string S,string T)
{
	int i = 1;
	int j = 1;
	int next[strlen(T)];
	get_next(T,next) ;
	while(i < S[0] && j <= T[0]){	//若i小于S长度且j小于T的长度时循环 
		if(j == 0 || S[i] == S[j]){		//两字符相等则继续 
			++i;
			++j;
			
		}
		else{
			j = next[j];			//j 退回合适的地方 
		}
	}
	if(j > T[0]){
		return i - T[0];	
	}
	else{
		return 0;
	}
}

以上就是KMP算法的实现了,但是KMP还是有一定的缺陷,比如下面这个例子:
在这里插入图片描述
在这个例子中,我们发现其实2、3、4、5步是多余的判断,我们发现,T串前5个位置的值是相同的,我们可以用首位next[1]的值去取代与他相等字符后续next[j]的值,所以我们可以这样改良我们的next数组:

/*
	nextval 数组
	
	T[0] 表示子串的长度 
*/ 
void get_nextval(string T,int *nextval)
{
	int i ,j;
	i = 1;
	j = 0;
	nextval[1] = 0;
	while(i < T[0]){
		
		if(!j || T[i] == T[j]){		//T[i+1] 表示后缀单个字符 
			++i;						//T[j] 表示前缀单个字符 
			++j;
			if(T[i] != T[j])	nextval[i] = j;				//如果与前缀字符不同,j 为 nextval 在 i 位置的值				
			else				nextval[i] = nextval[j];	//如果与前缀字符相同,则为前缀 
		}
		else{
			j = nextval[j];				//若字符不同,则j 回溯 
		}		 
	}	
}

我们将新的数组叫做nextval数组。
nextval数组的推导:
当我们计算出next数组后,我们计算nextval数组,当nextval数组的第 i 位与其next数组所指向的第 j 位相同,则第 i 位的nextval值为第 j 位next的值,如果不相同,则第 i 位nextval的值为next的值。

总结

我刚开始学习KMP算法,对其理解也不是特别深刻,如若有什么说的不对的,希望各位能够指正,我不胜感激。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值