Kmp算法
Kmp算法相比BF算法的优化之处在于当s[i] != p[j]时,i不回退,只有j回退。
1.下面我们来论证一下i为什么不用回退
如上图,在匹配到i = 7时失配,按照BF算法的思想,i’要回退到i’+1的位置,j要回退到j’+1的位置。这样的时间复杂度为O(m*n)。
但由以上例子不难发现,其实i’并不需要回退。这是为什么呢?
因为回退后再比较时,显然s3!=p0, s4!=p1.......s6!=p3。既然这样,就没有必要回退i。
有人会想,那要是一样呢?显然,都一样了那还比较干嘛,i当然也不需要回退。这样是不是感觉有点耍流氓?没办法,人家就是这么流氓。
2.我们已经知道i不需要回退,那么j呢?j要回退到哪里呢?相信聪明的你一定发现了,j是不需要回退到0的,因为s5 =p0, s6 =p1, 那么j只需要回退到下标为2的地方,接着比较久可以了。这时,你又该问了,我们人的肉眼可以看到j要回退到2,可是计算机要怎么知道呢?这个问题问的好,我将在下面解答你的疑问。
3.现在我们假设失配后,j要回退的位置为k, 猜想一下,k的值都和谁有关系呢?
下面跟我一起来见证奇迹:
聪明的你一定看出x等于i-k了,所以上式等于:
P0………………Pk-1 = Si-k………………Si-1 ( 1 )
由( 1 )可推出:
P0………………Pj-1 = Si-j………………Si-1 ( 2 )
缩小范围,得:
Px………………Pj-1 = Si-k………………Si-1
x = j-k
Pj-k………………Pj-1 = Si-k………………Si-1 ( 3 )
由( 1 )( 3 )得:
P0………………Pk-1 = Pj-k………………Pj-1 ( 4 )
由( 4 )可以清楚地看到,k的位置与S串(主串)无关,只与P串(子串)有关。是不是很神奇呢?
4.现在要引入一个更神奇的概念,一个神奇的next数组,用来保存子串里在任何位置出现失配时,j要回退到的位置,即k值。我们称为next[k]数组。
我们假设已经知道next数组的值,那么我们已经可以初步完成kmp算法了。代码如下:
int KMP(char *s,char *p,int pos)
{
int i = pos;
int j = 0;
int *next = (int *)malloc(strlen(p)*sizeof(int));
assert(next != NULL);
GetNext(p,next);
while(s[i]!='\0' && p[j]!='\0')
{
if(s[i]==p[j])
{
i++;
j++;
}
else if( j == 0)
{
i++;
}
else
{
j = next[j]; // i不回退,j回退到k
}
}
free(next);
if(p[j] == '\0')
{
return i-j;
}
else
{
return -1;
}
}
5.重中之重来了,如何求next数组呢?
你可以很自然地想到next[0] 和next[1] 的值,因为当子串第0个位置失配时,j仍然等于0,i++,而当子串第1个位置失配时,j要回退到第0个位置,i不变。So,,,,next[0] = 0, next[1] = 0。
接下来看看其他位置失配时j要回退的位置。
现在我们已知i = 0和i = 1时的next值,现在想要求i=2时的next值,那么我们可以把p[2]看作新增加的数,现在我们回过头想想k的含义,k是指子串失配时j要回退的的位置,并且我们已经知道此时P0………………Pk-1 = Pj-k………………Pj-1,next[i] = k,那么倘若pk = pj,则我们可以推出next[++i]=++k。
那如果pk不等于pj 呢?此时可以看作子串p0~~~pk与主串pi-k~~~pi匹配时在pk的位置失配,那么子串p0~~~pk中的下标k是不是要回退?(就像主串S串和子串P串匹配时失配后,只有子串中的j回退一样)这时我们的k要回退到哪里呢?
也许聪明的你已经发现了,我们在前面已经计算过next数组中从0~~~i的位置失配时的k值了,这里面显然包括在位置k时的next值,即next[k],所以当pk 不等于pi时,k要回退,即k=next[k]。
在这里我们还要讨论一种特殊情况,即子串p0~~~pk与主串pi-k~~~pi匹配时在pk的位置失配,子串p0~~~pk中的下标k回退到0时的情况,如果此时还让k=next[0]=0,那么继续匹配,仍然不相等,仍然要回退到k=0,再继续匹配,又不相等,又回退到0,~~~~~~哎呀累死我了,你发现了么?死循环了!!!
所以为了解决这种情况,我们要写一句else if (k == 0) { next[++i]=0; },这样i++了,而且也求得了next[i+1]的值,不会再出现死循环了。
void GetNext(const char*p,int *next)
{
int len = strlen(p);
next[0] = -1;
next[1] = 0;
int i = 1;
int k = 0;
while(i<len-1)
{
if(p[i]==p[k])
{
next[++i] = ++k;
}
else if(k == 0)
{
next[++i] = 0;
}
else
{
k = next[k];
}
}
}