【数据结构】详解KMP算法

详解KMP算法

1、基本算法

KMP算法主要用来快速地在主串S内pos位置及以后匹配到子串T的位置。一般的匹配思路:定义两个指针p1和p2,分别指向主串S 的pos位置和子串T的第一个字符,然后比对两个指针处的字符是否相等,如果相等继续比对下一个字符,直到子串所有字符比对完;如果在该比对过程中,出现比对不成功,则将p1往后移一位,p2重新返回到子串的第一个字符,然后重新开始比对;若比对成功,则返回子串T在主串S的位置,否则返回0。其具体的算法代码如下所示:

int index(string source, string sub, int pos){
	char *ptrSource = &source[0];
	char *ptrSub = &sub[0];

	int len_source = source.length();
	int len_sub = sub.length();

	int i = pos, j = 1;
	while(i <= len_source && j<= len_sub){
		if(*(ptrSource + i - 1) == *(ptrSub + j - 1))
		{
			i++;
			j++;
		}
		else{
			i = i - j + 2;//i = (i - (j - 1) + 1)
			j = 1;
		}
	}
	if(j > len_sub)
		return (i - len_sub);
	else
		return 0;
}

上述基本的算法虽然容易理解,但是会存在第一个指针p1不断回退以及算法复杂度较高的现象,为此D. E. Knuth和J. H. Morris以及V. R. Pratt提出了一种KMP算法,该算法主要的改进在于p1不会回退,子串在匹配时尽量的靠右“滑动”匹配主串T。

为了形象的说明,举个例子:


第一趟匹配:

此时第三个字符不匹配,p1停留在主串T的T[2]处(下标从0开始)。

第二趟匹配:

此时第五个字符不匹配,p1停留在主串T的T[6]处。


第三趟匹配:


此时匹配成功,但是此轮匹配时,p2指向的不在是子串T的首字符串,而是第二个字符T[1]。并且在这三轮的匹配中,指向主串S的指针p1没有回退过。因此KMP算法中的重点在于确定下一轮匹配中,p2指针指向子串T的哪个字符,或者说,从子串T中的哪个字符与当前指向主串S的指针p2所指向的字符开始作比对。

2、基本原理

假设主串S如下所示:


子串T如下所示:


假设当子串的第j个字符在主串的第i个字符处匹配失败,并且重新开始新的一轮匹配时,主串中的第i个字符应该与子串的第k个字符重新比对(关系如下图所示)。

主串S:


子串T:


此时将子串滑动;

主串S:


子串T:


子串T滑动后,第k个字符和主串的第i个字符重新开始新一轮比对。

子串T滑动前,对于主串S,此时Si-j+1~Si-1和子串T中的P1~Pj-1是已经匹配成功的(共匹配成功j-1个字符)。子串T滑动后,子串T的第k 个字符(k < j)跟主串S中的第i个字符进行比对(假设的情况),即Si-k+1~Si-1和子串T中的P1~Pj-1 是匹配的字符串(共匹配k-1个字符)。

经过对比可以发现:在子串中,Pj-k+1~Pj-1对应的字符串和P1~Pk-1对应的字符串相同。

于是,令 next[j] 表示当子串 T 中的第 j 个字符与主串 S 的当前字符( p1 指向的字符)匹配失败时,在子串 T 中需要重新和主串 S 中该字符进行比对的字符的位置。指向子串 T 的指针 p2 然后指向该字符,再进行比对。如果发现该新位置的字符仍然匹配不了,就继续将 p2 指向 next[next[j]] 位置的字符 … 一直进行下去。此时如果发现 p2 指向的子串的字符和 p1 指向的匹配了,则将 p1 和 p2 都加一让其指向下一个字符串。如果直到 next[…next[j]…]==0 ,然后将 p2 指向子串 T 的第一个字符, p1 指向主串 S 的下一个字符。

KMP算法代码如下:

int KMP(string source, string sub, int pos){
	char *ptrSource = &source[0];
	char *ptrSub = &sub[0];
	
	int len_source = source.length();
	int len_sub = sub.length();
	
	int *next = new int [len_sub];
	if(!next) exit(-1);

	getNext(sub, next);

	int i = pos, j = 1;

	while(i <= len_source && j <= len_sub){
		if(j == 0 || *(ptrSource + i - 1) == *(ptrSub + j - 1)){
			i++;
			j++;
		}
		else{
			j = next[j-1];
		}
	}
	if (j > len_sub) return (i - len_sub);
	else return 0;
}

以上KMP算法是基于子串对应的next数组的。此时,问题转化为怎么计算子串T对应的next数组了。

3、next数组

由定义得知,next[1] = 0(子串第一个字符都不匹配的时候,直接将子串整体移动一个字符);

假设next[j] = k,则表示当子串T的第j个字符串与主串S中的当前字符比对失败时,重新将子串T 中的第k 个字符(k < j)和主串当前字符进行比对。因此会有:在子串中,Pj-k+1~Pj-1对应的字符串和P1~Pk-1对应的字符串相同

求next[j+1]?(数学归纳法)

当Pk==Pj时,如下图所示,当这两个字符相等时,当子串T的第j+1个字符和主串S匹配失败时,可以直接将子串的第k+1个字符对齐主串的当前字符。因此next[j+1] = k + 1。



当Pk != Pj时,如下图所示,问题转化为子串T1的第j个字符Pk和子串T2第k个字符进行比对的情况。而此时需要按照子串T的next[k] = k1处的字符对齐到子串T1的第j个字符。如果此时的Pnext[k] == Pj,则表示next[k+1] = k1 + 1,如果此时Pnext[k] 和 Pj仍然不相等,则继续类推,直到Pj和子串中的某个字符匹配或者没有找到任何匹配的,则next[k+1]=1。

求一个字符串对应的next数组算法代码如下:

void getNext(string sub, int next[]){
	int i = 1;
	next[0] = 0;
	int j = 0;
	char *p = &sub[0];
	int len_sub = sub.length();

	while (i < len_sub){
		if(j == 0 || *(p + i - 1) == *(p + j - 1)){
			++i;
			++j;
			next[i-1] = j;
		}
		else
			j = next[j-1];
	}

}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bible_reader

如果觉得文章有用,欢迎打赏支持

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

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

打赏作者

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

抵扣说明:

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

余额充值