KMP算法

闲着没事想复习下KMP算法,自己推导了下,没想到搞了很久,整理记录下。


先从一般的字符串查找算法推导。

现在我们要在字符串S里面查找字符串D,则一般算法是

position = 0
WHILE position < S.length //枚举D在S中的位置position
	result = Match(S, D, position); //看在position位置上D能匹配S几个字符,从左到右比较匹配
	IF (result == D.length) RETURN position; //若D在position上能批评D.length个字符,表示D就出现在这个位置
	position += 1; //尝试下一个位置
END
return -1; //没有找到D
这个算法时间复杂度比较高,上界是两个字符串长度的乘积。

我们发现,这个算法有时候做了很多无用功,比如

D = "abcd"

假设 position=0 时, Match(S, D, 0) = 3。那我们肯定知道 S = "abc????..." (?表示未知字母,...表示省略后面的字符),如果我们下一次让position = 1,就是 "abcd" 和 "bc??" 匹配,直接从第一位就可以看出是不可能相等的(‘a’ != 'b'),所以没有必要尝试这个位置。同样的,position=2时(“abcd" 匹配 "c????")也不可能相等;position=3时,"abcd" 和 “????” 匹配,有可能相等,才尝试这个位置。

综上所述,在position=0时得到了Match的结果3。我们利用这个结果3,以及D的前三位字符 这些信息 确定了position=1和2时肯定不可能匹配成功。所以可以直接尝试position=3,减少尝试匹配次数。

充分利用信息,减少不必要的操作。

我们于是想到,利用D的内容,和每次Match的结果(从开头起多少个字符匹配成功),可以推断出position下一次的位置。我们设这个过程为 Next(D, count),position的尝试变为 position += Next(D, result)。对于上面的情况就是 position=0 时, Match(S, D, position) = 3,Next(D, 3) = 3; position += 3; ==> position=3时blablabla……

即,代码变为

position = 0
WHILE position < S.length //枚举D在S中的位置position
	result = Match(S, D, position); //看在position位置上D能匹配S几个字符
	IF (result == D.length) RETURN position; //若D在position上能批评D.length个字符,表示D就出现在这个位置
	position += Next(D, result); //尝试分析得到的下一个位置
END
return -1; //没有找到D
下面问题就是如果设计Next(D, count)这个函数。

我们可以知道在算法运行过程中D不会改变,而count是一个0到D.length的数值。我们求出count = 0 到 D.length 下 Next(D, count) 的值,存到一张表里,每次查表就行了。

next = GenerateNextTable(D)
position = 0
WHILE position < S.length //枚举D在S中的位置position
	result = Match(S, D, position); //看在position位置上D能匹配S几个字符
	IF (result == D.length) RETURN position; //若D在position上能批评D.length个字符,表示D就出现在这个位置
	position += next[result]; //尝试分析得到的下一个位置
END
return -1; //没有找到D

下面重点就是GenerateNextTable(D)的过程,也就是产生next 数组的过程。next 数组可以简单求出,也可以用更快的算法产生。(由于不会做图,下面有一堆描述,为方便不同种类的阅读,灰色的是说明,可忽略;绿色是定义;红色是算法流程公式;蓝紫色是建议

如果你尝试了,会知道直接递推出next数组是比较困难的。我们观察发现,若next[i] = k, 则有 D[0..i-k-1] = D[k, i-1]。即把D的前i位拿出来当一个新字符串str,str的前面i-k个字符和str的后面i-k个字符是相等的。而且我们发现,按照next的定义,(i-k)一定是个最大值使str前i-k位和后i-k位相同(k!=0)。因为k相当于position跳过的步数,我们不能跳过有可能匹配的位置。比如说匹配过程中知道S从position开始是这样一个情况abcde???... (没加双引号:字母代表变量字符,而非确定字符;?代表未知字符),那么若abc = cde,我们就知道position 增加 2 是可以的(abcde... 匹配 cde???...,前三位相同,后面不知道是否相同,可能会匹配成功),若ab=de,那么position增加3也是可以的,但是我们肯定是让position增加2,因为不能漏掉这个可能。也就是说k有多种可能时取最小,则i-k最大的值。(有点绕,用笔画画会清楚不少)

于是我们定义这样一个操作,对于一个字符串str,它最长有w位前缀等于它的w位后缀,我们称w为str的P值。我们可以知道,next[i] 就等于 i 减去 D[0..i-1 ]的P值(若D[0..i-1]构成的字符串前w位和后w位相同,则若Match结果为i,我们可以直接把position 加上 i-w,请拿纸和笔推导下)。我们先求出每个i,D[0..i-1] 的P值P[i],然后next[i] = i - P[i]

求P[i]的话这样做,假设我们知道P[i-1] = k,那么如果 D[i-1] == D[k] 则 P[i] = k+1(即为新添的一个字符正好等于前缀后面那个字符,请拿纸和笔推导具体下标的产生如果D[i-1] != D[k],看起来会很难办,难道要一个个向前枚举k了么?不,最精华的一点到来了,我们令k=P[k],继续上面的过程虽然P[k]所作用的区域已经是D[0..k-1]了,但是我们之前有P[i-1] = k,可以知道D[0..k-1] 和 D[i-1-k..i-2]是相同的,那么D[k-1-P[k]..k-1]也会和D[?..i-2]相同(建议画图理解)。这样减少了k不必要的枚举,且不会漏掉最优解(仔细考虑)。此外我们预设P[0] = -1,k等于-1时令P[i]=0 (k减少到-1意味着P值为0)。

把上面所述红色部分提取出来就能得到next的产生了,时间复杂度是O(n)的,n为D的长度,不证明了,直接给出C语言函数

int *GenerateNextTable(const char *D) {// next[i],前i个字符匹配成功,需要前进几步 
	int L = strlen(D); 
	int *next  = (int *)malloc(sizeof(int) * L);

	int P[L+1], i, j;
	P[0] = -1;
	for (i=1; i<=L; i++) {
		j = P[i-1];
		while (j>=0 && D[j]!=D[i-1]) {
			j = P[j];
		}
		P[i] = j + 1;
	}
	
	next[0] = 1;
	for (i=1; i<L; i++) {
		next[i] = i - P[i]; 
	}
	
	return next;
}

同样的,根据更上面描述,我们写出另外两个函数的C代码

int Match(const char *S, const char *D, int start) {
	int i;
	for (i=0; D[i]; i++)
		if (S[i+start] != D[i]) break;
	return i; 
}

int KMP(const char *S, const char *D) {
	int position = 0, L = strlen(D);
	int *next = GenerateNextTable(D);
	while (S[position]) {
		int result = Match(S, D, position);
		if (result == L) break; //find it
		position += next[result];
	}
	free(next);// free memory
	return S[position] ? position : -1; // return answer
}

OK,经测试,这三个函数工作的很好,事情到此结束了么? 不!

充分利用信息,减少不必要的操作。

想一下,我们在查找过程中,利用到了这次匹配前 i 个成功了以及D的内容这两个信息,推断出下一个position的位置,减少了不必要的Match操作。可是由一个信息我们还没有利用到,就是第 D[i+1] 和 S[position+i+1] 不想等。

试想,假设 我们用 "aa" 匹配到了 “a?...", 按照上面所求的结果,position应该增加1,因为前1个成功了,而next [1]总归是1的。但是我们发现,?和a不相同这个信息可以让我们知道,position这次可以直接增加2,而非1。我们可以根据这个再次对程序进行优化。(这一步到具体代码有点跳跃,懒得详细描述了)

int *GenerateNextTable(const char *D) {// next[i],前i个字符匹配成功,需要前进几步 
	int L = strlen(D); 
	int *next  = (int *)malloc(sizeof(int) * L);

	int P[L+1], i, j;
	P[0] = -1;
	for (i=1; i<=L; i++) {
		j = P[i-1];
		while (j>=0 && D[j]!=D[i-1]) {
			j = P[j];
		}
		P[i] = j + 1;
	}
	
	next[0] = 1;
	for (i=1; i<L; i++) {
		int t = P[i];
		while (D[t] == D[i] && t>=0) {
			t = P[t];
		}
		next[i] = i - t;
	}
	return next;
}

至此,我所知道的KMP大概就优化到这里。时间负责度为O(M+N)。


总结:
这篇文章并不倾向与对KMP详细的过程解释说明以及复杂度证明,而更在于一步步推导算法的思维过程。一开始用一个朴素的查找算法,通过 充分利用已知信息 这一思想不断的改进设计算法。对于KMP的实现来说,本文的代码不算优秀,只是体现下设计推导算法的思维过程,而且和其他实现方式有所差别(其他大多数实现中的next 数组为本文的P数组,查找循环中可以不后移指针等好处blabla)。

(ps:水平有限,若文中有错误之处希望指出改正。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值