相信很多人都知道KMP算法,但是我也相信大部分人并没有真正的理解它,许多人对KMP的理解都是通过画图,对着代码一步一步地去推敲,理解。这样学习KMP算法是没有错的,但是如果想要掌握这个算法的灵魂,还需要明白下面几点:
第一 KMP算法的目标是什么?
KMP算法的目标就是不让源串回溯,提高匹配的效率。
第二 KMP是如何实现不回朔的?
是通过next数组。
第三 next值代表什么?
next值代表如果源串和目标串字符匹配失败,源串匹配失败的字符应该继续和目标串中的哪个字符继续匹配。
第四 next值是什么?
next值是目标串(模式串)当前字符之前的字符串最大匹配前后缀长度加1。
在讲第五点之前,先看看下面的代码,具体如下:
void get_next(String T, int * next)
{
int i, j;
i = 1;
j = 0;
next[1] = 0;
while (i < T[0])
{
if (j == 0 || T[i] == T[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
相信这段代码,大家都很熟悉,写得好精简,可是越精简的东西,越难理解,我今天的目标就是让你们彻底明白这段代码背后的思想。
第五点 next链接,代表什么?
前面我们已经讲解了next值代表什么,可是在上段代码中,最难理解得是 j = next[j],那么这个next链,究竟代表什么?下面我告诉你们它代表的是某字符前所有的前后缀,并且从长到短地排列起来。
第六点 计算目标串某个位置的next值的算法是什么?
我们可以分为四种情况:
第一种情况就是在目标串中第一个位置的字符,这个位置前面没有字符串,所以如果和源串字符匹配失败,就应该返回0,告诉源串前进一个字符再重新从头匹配,具体如下图所示:
因为我们的目标串从1开始,这里0表示让源串前进到下一个字符,如果我们的目标串从0开始,那么我们就返回-1,这里的-1表示让源串前进到下一个位置。本篇我们假设目标串从1开始,所以我们返回0。
第二种情况是在目标串中第二个位置的字符,这个位置前面只有一个字符,如果第二个位置的字符和源串字符匹配失败,理所当然地应该让目标串中的第一个字符继续和源串中匹配失败的字符继续比较,所以这个位置的next值应该为1,表示当源串字符和第二个字符不匹配的情况下,移动目标串,让它和目标串中的第一个字符继续比较,如果还不匹配,这时因为目标串中第一个字符的next值为0,所以源串会移动到下一个字符,重新和目标串比较,具体如下图所示:
因此当目标串的第二个字符匹配失败的时候,只需要让源串的字符和目标串的第一个字符去匹配就可以,因此第二个字符的next值为1。
第三种情况是目标串中的字符前面存在两个字符以上(包括两个字符)的情况,即从第三个字符开始,再根据其前面的字符串是否包含匹配的前后缀分为两种情况,一种情况是包含前后缀匹配的情况,另外一种是不包含前后缀匹配的情况。第三种指的是不包含前后缀匹配的情况,因此当源串中的字符和目标串中的字符不匹配的时候,我们应该让源串中匹配失败的字符和目标串中的第一个字符去比较,因此它们的next值应该为1,具体如下图所示:
正如上图所示,当源串中的e和目标串中的k不匹配的时候,源串中的e应该和目标串中的第一个字符a继续比较,具体如下图所示:
因此在这个目标串中,从第三个字符c开始到最后的每一个字符的next值都应该为1。
第四种情况是目标串中从第三个字符开始,其前面的字符串包括相互匹配的前后缀,比如下面的情况:
如上图所示,当源串中的e和目标串中的x不匹配的时候,应该让源串中的e和目标串中的c去比较,因为x前面的字符串相互匹配的前后缀为ab,所以像这种情况,它的next值为前后缀匹配的长度加1。
所以,综述上面4种情况,next的计算方法如下:
所以求取Next值的最大难点是求取最大前后缀匹配长度。
第七 计算前后缀最大匹配长度的一般逻辑是什么?
假设我们要求取的位置是m,下标从1开始,目标串为T,那么一般的算法如下:
//T目标串
//m是需要计算最大前后缀长度的位置
int GetMaxLen(char * T, int m)
{
//默认没有匹配的前后缀
int nMaxLen = 0;
int n = 0;
if(!T)
return -1;
if( m < 3)
return -1;
for( n = m-2; n >= 1; n--)
{
//找到前缀的最后一个字符
if(T[m-1] == T[n])
{
nMaxLen = n;
bSuc = true;
for( i = 1; i <= n; i++)
{
if(T[i] != T[m-1 - nMaxLen + i])
{
bSuc = false;
break;
}
}
if(bSuc)
return nMaxLen;
}
return 0;
}
这个算法很容易理解,但是我们今天要讲的不是它,是上面的那个精简代码背后的灵魂,从这个一般算法的代码中,我们可以看到在求取最大匹配的过程中,n从m-2开始,一步一步地遍历,这样会走很多冤枉路,因此,我们不需要这么做,因为效率很低下。
如果需要明白精简代码,我们需要知道它利用了哪些规律,具体如下:
这个规律具体如下:
给一个目标串T,当n>=3时,如果t[n]的前后缀最大匹配长度为k,那么t[n+1]的最大前后缀长度为k+1。
证明:
假设t[n+1]的最大前后缀匹配长度为m,并且m>k+1,那么t[n]的匹配前后缀应该为m-1,但是因为m>k+1,所以
m-1>k,因此与假设前提相违背,所以t[n+1]的最大前后缀长度为k+1。
具体如下图所示:
对于上面的这个字符串,d的前后缀最大匹配长度为3,所以e的前后缀最大匹配长度应该为4,现在假设e的前后缀最大匹配长度为5,那么应该是下面的情况,我们不要看是否匹配,只看匹配的字符数,具体如下:
如上图所示,d的前面匹配数量应该是4,但是d的前后缀匹配是3,与现实相违背,所以最大不可能超过4。
这个规律告诉我们n不必从m-2开始,而是从next[m-1]开始,除了这个,还利用了next链,其实如果我们画图,一步步确认,就可以发现next链代表的是m-1的所有前缀,并且是从长到短地组织起来,所以,我们只需要让T[m-1]和所有的前缀的下一个字符比较就可以,如果比较成功,就证明存在前后缀匹配的情况,否则,就用T[m-1]和T[1]比较,如果相等,则证明Next[m]=2,否则证明m前没有匹配的前后缀,那么Next[m]=1,这个就是上面那段精简代码的背后思想,如果还不明白,按照我的提示,一步一步画图,就会全部明白,因为我已经把最难理解得东西告诉了你们。