KMP算法,适用于寻找子串的问题,其核心思路是先将子串进行自我匹配,构建出一个next数组。当寻找子串时匹配失败,就可以回到下标为当前位置的next数组里,继续开始匹配,避免了每次都得从头再来的麻烦,以此来节省时间,达到线性时间的复杂度。
构建next数组的核心在于自匹配。首先遍历字符串,寻找与0号等同的字符,找到就记录下当前下标到next数组内,并同步检查下一位是否一样等同,是的话继续下一位,依此类推。中途若出现不等同的情况,就从0号重新开始进行匹配;但需要注意的是,这时字符串的遍历是不需要重新开始的。
KMP的具体实现有多种不同的变形,下面的是其中一种情况,例子与百度百科的相同,但算法过程是不一样的,要注意区别。
比如当字串为abcabcddes时,首先我们对其构建next数组,为-1000123000。过程如下:0位默认是-1,即next[0]=-1,因为不可与自己匹配。然后对1号的’b’和0号的’a’进行比较,不相等,所以next[1]=0。在这里,[1]表示1号字符,0表示回到0号字符。依此类推,直到3号字符,也就是’a’,开始同步移向下一位,所以next[3]=1(注意不匹配时,只是在进行遍历,匹配的字符仍然停留在0号)。然后发现4号字符和1号字符仍然相等,都是’b’,所以next[4]=1。同理,next[5]=2,next[6]=3。然后,7号字符’d’与4号字符’a’并不等同,所以next[7]=0,表示需要重新开始匹配(注意遍历仍在进行),回到了0号字符’a’。往后再也没有出现匹配的情况,所以next[8] = next[9] = next[10] = 0。
构造完next数组,我们就可以开始正式进行匹配了。KMP匹配和一般的匹配差别不大,一开始也是在父串中寻找与字串0号字符相同的位置,若然找到了就开始同时检查下一个是否匹配。差别在于匹配失败时,朴素算法是重新回到最开始的字符进行重新匹配;而KMP算法则是回到匹配失败时的字串下标对应的next数组中,进行重新匹配。
比如在dabcabcabcabcddess中寻找abcabcddes时,可以发现0号字符不匹配,但1号字符出现了匹配,并以此开始检查是否为全匹配;可是到了父串的的7号字符‘a’时,与子串中的6号字符‘c’失去了匹配的特性。在朴素算法中,这时我们需要回到父串的2号字符(因为本次是从1号字符开始检查全匹配的,但是失败了,所以需要回到本次开始的下一个字符),重新匹配子串的0号字符。但是在KMP算法中,这时我们回到的是子串的next[6]号字符,也就是3号字符’a’,而父串可以继续遍历不受影响(之前可是要回到2号字符,基本等同于重新开始啊)。仔细观察就可以发现,父串可以继续遍历是因为7号字符‘a’之前的“abc”仍然和字串的前3个字符相匹配,就省去了重复匹配的麻烦,而这正是next数组的功劳。
所以,当字串自匹配程度越高时,KMP算法的优势也就越明显。
假设现在要在字符串a中寻找字符串b,并返回b第一次出现在a中的下标,如果不存在的话就返回-1。由于字符串是从0开始计算的,把上面的下标信息全部-1即可。
计算next数组的函数如下:
void f( string b, int next[] ) {
next[ 0 ] = -1;
int j = 0;
for ( int i = 1; i < b.size(); i++ ) {
next[ i ] = j;
if ( b[ i ] == b[ j ] )
j++;
else if ( j != 0 )
j = next[ j ];
}
}
而匹配函数为:
int match( string a, string b, int next[] ) {
int j = 0;
for ( int i = 0; i < a.size(); i++ ) {
if ( a[ i ] == b[ j ] )
j++;
else if ( j != 0 ) {
j = next[ j ];
i--;
}
if ( j == b.size() )
return i - b.size() + 1;
}
return -1;
}