在介绍KMP算法之前,先看下一般得解法。
主字符串以下简称位(T)
模式串以下简称位(P)
题目: 在字符串T"abcdefghijk"中查找P"abce",如果出现就返回P得具体位置,否则返回-1.
一般的解法
int my_strstr(char * T, char * P) {
int lenT = strlen(T), lenP = strlen(P);
int i = 0, j = 0;
int res = -1;
for (i = 0; i <= (lenT - lenP); i++) {
for (j = 0; j < lenP; j++) {
if (T[i + j] == P[j]) {
continue;
}
break;
}
if (j == lenP) {
res = i;
break;
}
}
return res;
}
上面代码没有问题,但是还可以进行优化。
如何优化
参考上面的算法,我们串中的位置指针i,j来说明,第一个位置下标以0开始,我们称为第0位。下面看看,如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置(i=3)前面除了第一个A之外再也没有A了,我们为什么能知道主串前面只有一个A?因为我们已经知道前面三个字符都是匹配的!(这很重要)。移动过去肯定也是不匹配的!有一个想法,i可以不动,我们只需要移动j即可,如下图:
或者如下图所示
总结:当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。
公式:当T[i] != P[j]时
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]
该规律是KMP算法的关键,KMP算法是利用待匹配的子串自身的这种性质,来提高匹配速度。该性质在许多其他中版本的解释中还可以描述成:若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位)。我们将这个解释定义成最大重复子串解释。
这里面的前缀集表示除去最后一个字符后的前面的所有子串集合,同理后缀集指的的是除去第一个字符后的后面的子串组成的集合。举例说明如下:
在“aba”中,前缀集就是除掉最后一个字符’a’后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;
在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;
在“abcabcdabc”中,前缀集是{a,ab,abc,abca,abcab,abcabc,abcabcd,abcabcda,abcabcdab},后缀集是{c,bc,abc,dabc,cdabc,bcdabc,abcdabc,cabcdabc,bcabcdabc},二者最长重复的子串是“abc”,k=3;
下面我们用这个解释,来再一次手动求解上面的过程:
首先如下图所示:
如图:C和D不匹配了,我们要把j移动到哪?j位前面的子串是ABA,该子串的前缀集是{A,AB},后缀集是{A,BA},最大的重复子串是A,只有1个字符,所以j移到k即第1位。
再分析下图的情况:
在j位的时候,j前面的子串是ABCAB,前缀集是{A,AB,ABC,ABCA},后缀集是{B,AB,CAB,BCAB},最大重复子串是AB,个数是2个字符,因此j移到k即第2位。
上面说的,如果分解成计算机的步骤,则是如下的过程:
1)找出前缀pre,设为pre[0~m];
2)找出后缀post,设为post[0~n];
3)从前缀pre里,先以最大长度的s[0~m]为子串,即设k初始值为m,跟post[n-m+1~n]进行比较:
如果相同,则pre[0~m]则为最大重复子串,长度为m,则k=m;
如果不相同,则k=k-1;缩小前缀的子串一个字符,在跟后缀的子串按照尾巴对齐,进行比较,是否相同。
如此下去,直到找到重复子串,或者k没找到。
根据上面的求解过程,我们知道子串的j位前面,有j个字符,前后缀必然少掉首尾一个字符,因此重复子串的最大值为j-1,因此知道下一次的j指针最多移到第j-1位。
我为什么要补充上面这段说明,是因为该说明能便于我们理解下面的求解next数组的过程,上面实际也是指出了人工求解next[j]的过程。不知道next[j]为何物没关系,看到下面的定义以后,请到时再绕回来回味就行了。
next数组
在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。另一个非常有用且恒等的定义,因为下标从0开始的,k值实际是j位前的子串的最大重复子串的长度。请时刻牢记next数组的定义,下面的解释是死死地围绕着这个定义来解释的。
int * getNext(char * P) {
int len = strlen(P);
int i, k;
int *next = (int *)malloc(sizeof(int) * len);
next[0] = k = -1;
i = 0;
while(i < len - 1) {
if (k == -1 || P[k] == P[i]) {
next[++i] = ++k;
} else {
k = next[k];
}
}
return next;
}
有了next数组之后就一切好办了,我们可以动手写KMP算法了:
int my_strstr(char * T, char * P) {
int lenT = strlen(T), lenP = strlen(P);
int i = 0, j = 0;
int res = -1;
int *next = getNext(P);
i = j = 0;
while(i <= lenT - lenP + j + 1) {
if (j == -1 || T[i] == P[j]) {
i++, j++;
} else {
j = next[j];
}
if (j == lenP) {
res = i - j;
break;
}
}
free(next);
return res;
}
原文链接 : https://www.cnblogs.com/dusf/p/kmp.html