算法核心思想:
对于常规的字符串匹配,一般都是一路穷举过去,如下图。
(例如要在字符串T="abcabcabd"中寻找字符串S="abcabd")
黄、灰色代表被检查过,黄色代表相等,灰色代表不相等,没有颜色标记代表不用检查,可以看出常规的办法,每次都要从头开始检查,一旦检查到不相等,S串只能向右移一位,然后重新从头检查,做了很多重复的工作。
然而KMP算法的过程是这样的:
明显的看到,检查的次数减少了(直接跳过了原来常规的第二次和第三次检查),在第一次检查完后S串直接右移三位,而且在第二次检查的时候,也并没有从开头检查。
KMP算法的原理:
如图,此时黄色标记为对应竖直T中字符是相等的,灰色代表不相等(和上面的表达的【是否检查】意义不同)。
为了阐述方便,将处于不同位置的两个S串在竖直方向上连续重合、在竖直上对应的字符相等、左侧没有灰框的区域称为交集。
铺垫结束,开始阐述。
在一次S串匹配失败后,如果出现解,则接下来的移动出现解只有三种可能:
1.向右移动,有交集
2.向右移动,有重合没交集
3.向右移动,没重合没交集
针对于第一种情况,KMP算法核心就是依此产生的:如果在一次匹配失败后,假设存在移动1使解出现,由于解一定和原来S串有交集,所以这个解只能在与原来S串有交集的S串中找,没交集的肯定不行,所以KMP算法中存在S串右移多步的情况。并且,交集处的字符因为相等,所以移动后的S串已经间接和对应的T中字符比较过,所以移动后的S串不需要从头和T串比较。如果移动1无法产生解或者说不存在移动1,那只能够向右移动的更远,也就是选择移动2,移动2也没有,那只能移动的更远,选择移动3。
在一次匹配失败后,S要右移,为了防止错漏,移动的选择必须由近到远的(下文有说原因),所以我们必须要确定是否存在移动1,即确定S串右移是否存在交集,如果存在,当前交集的最长是多少,必须要从最长的交集开始匹配(为了防止错漏,还是由近到远的原则),由交集的性质我们知道,交集肯定是从黑框左边开始的,如下图,我们发现交集中S串的字符所对应的下标,没错,上面的S串的下标正是从黑框左边的第一个数起, 而下面的则是从 S[ 0 ]开始数起,也就是说,交集本质其实是, 从S[ 0 ]开始的n个连续的字符构成的字符串和黑框前的n个字符串相等的意思。
很明显n越大,也就是交集越长,也就是相等的字符串部分越长的时候,两个位置的S串竖直重合更多,第二个离第一个越近,我们用一个数组next来记录交集信息,next[ i ]记录的是当当前位置 S[ i ]是一个灰框(也就是不和T相等的时候)、S[ i ]之前不存在灰框,S[ i ]前 next[ i ]个字符和从S[ 0 ]开始数起的next [ i ]个字符是相等的。
对于 s="avoavd",next[ 5 ]=2;
很明显 S[ 0 ] 、S[ 1 ] = “av”,S[ 3 ]、S[ 4 ]="av"
这样一来,在匹配的过程中一旦出现黑框,就停止匹配,根据出现黑框的S的下标,利用next数组判断前面是否有交集,如果有,右移S串至最大的交集的位置(因为此时向右移动的最少,不会错漏),然后继续重复判断,如果没有,就要根据情况选择移动2或者移动3了。
这里先叉开next数组,填一下之前的由近到远的原则是怎么回事。
移动的选择是由近到远的:
三种移动的选择优先顺序是,移动1>移动2>移动3,三种移动的移动距离很明显是由近到远的,参照一开始的暴力常规匹配,S串是每次向右移动一个单位,是为了避免错漏,同理,可以选择移动1的情况下,选择了移动2或3,也会造成错漏,如:
以下是在初始S之后的三种移动选择,可见,由于没按照移动顺序从近到远的原则,选择之后只能在该选择的基础上往右移,因此部分可能没有被检查就被忽略掉了
(接下来的颜色标记代表的意思, 黄、灰色代表被检查过,黄色代表相等,灰色代表不相等,红色代表不用检查肯定相等,没有颜色标记代表不用检查。)
另外,在进行移动1的时候,一定要选交集最长的开始移动匹配,否则违背了由近到远的原则,也会导致错漏,如:
NEXT数组:
next数组还有一个理解,next[ i ]代表的是当S[ 0 ]...S[ i-1 ]都和T对应的字符相等时,然而S[ i ]不相等时,S串将右移,使得S[ next[ i ] ]移动到S[ i ],如
T="abababdccq", S="ababd"
next中的内容如下:
模拟匹配过程:
在第一次匹配过程中,发现S[ 4 ] ! = T [ 4 ],由于next[ 4 ] = 2,所以要右移S串使S[ 2 ]移动到S[ 4 ]所对应的位置,然后进行第二次匹配
在第二次匹配,由于是S[ 0 ] = =S [ 2 ],S [ 1 ] = = S [ 3 ],而且S[ 2 ]、S[ 3 ]已经对应T中字符比较过了,所以
S[ 0 ]、S[ 1 ]不需要比较,直接从S[ 2 ]开始和T[ 5 ]开始比较.
这里再解释下next[ i ]的内容,它就是表达,当前S[ i ]不匹配时,S[ next[ i ] ]串应该右移至S[ i ]的位置,(-1是额外规定的含有额外的意义),所以在如果要 填写next[ i ]中的内容是的时候,直接想,S[0]...S[i-1]都匹配,然而
S[ i ]不匹配,S串右移使得S[ X ]到S[ i ]的位置,这个X是什么即可。
next[ 0 ]=-1, 说明S[ 0 ]和当前的T [ m ]比较过了 (T[ m ]是当前S[ 0 ]竖直上对应的字符,下同),应该移动S串使得下次直接比较S[ 0 ]和T[ m+1 ];
next[ 1 ]=0, 说明S[ 0 ]与当前T[ m ]需要比较(T [ m ]是当前S[ 1 ]所对应的字符);
next[ 2 ]=-1, 说明S[ 0 ]和当前的T [ m ]比较过了,应该移动S串使得下次直接比较S[ 0 ]和T[ m+1 ];
next[ 3 ]=1, 说明S[ 0 ]==S[ 2 ],应该右移S串使S[ 1 ]到T[ m ]的位置,比较S[ 1 ]和T[ m ]的值
next[ 4 ]=2, 说明S[ 0 ],S[ 1 ] = = S[ 2 ],S [ 3 ],应该右移S串使得S[ 2 ]到T [ m ]的位置,比较 S[ 2 ]和T [ m ];
以下是网上拿来的总结,看一看有助于想到各种各样的填NEXT [ i ]的情况:
例子:
最后是 next数组的生成华丽超短代码,好好咀嚼咀嚼吧
void get_next(char *s,int *next)
{
int j,k;
next[0]=k=-1,j=0;
while(s[j]!='\0')//作用:找当前断点的最长前缀
{
//begin
if(k==-1 || s[j]==s[k])//前缀相同
{
j++,k++;
if(s[j]==s[k])//前缀相同且断点都相同
next[j]=next[k];
else//前缀相同断点不同
next[j]=k;
}
else //前缀不同,去找到相同的前缀,或到最后为空
k=next[k];
}
}
前缀的意思:对于s="avoavd",那么对于S[ 5 ],next[ 5 ]=2它的前缀就是S[ 0 ],S[ 1 ],也就是从S[ 0 ]数起的next[ 5 ]个字符.(为了方便就不起另外一个名字了,其实前缀本来的意思就是任意前面连续的n个字符,这里特指相等的前缀).
同时,这里有一个性质:S[ k ]与 S [ next[ k ] ], S[ next[ k ] ]与S [ next[ next [ k ] ] ]......他们之间的前缀是存在公共前缀的(这里 S的下标不为 0,如果下标为 0,S[ 0 ]的前缀就是空,其实空也可以看成一个公共前缀.)
如:S="acacacad", 当K=7时,S[ 7 ]的前缀" a c a" ; 当K=3,S[ 3 ]的前缀"a"; 当K=1时,S[ 1 ]的前缀" a ";
所以他们的公共前缀是"a",这性质有什么用呢? 代码的最后一句 k=next[ k ]就是利用这个性质
不妨用S="acacacade",j=7,k=3,从循环的begin开始,大概就可以清楚k=next[ k ]的作用了.
此文参考网上众多文章,特别此篇,http://blog.chinaunix.net/uid-27164517-id-3280128.html。
另外,链接中介绍了next的第二种表示方法,虽然没这么高效,还是在此贴一下代码:
void get_next()
{
int j, k;
next[0]=k=-1,j=0;
while(s[j]!='\0')
if(k == -1 || s[j] == s[k])//有相同的前缀
next[++j] = ++k;
else//没有相同前缀,找相同的前缀
k = next[k];
}
写的很仓促,冗长,要靠读者自己压缩找精华了,其实理解了关键,KMP的核心内容是很少的。
难免有写错疏漏的地方,请各位指正。