KMP是一个高效的字符串匹配算法,它是由三位计算机学者 D.E.Knuth 与 V.R.Pratt 和 J.H.Morris 同时发现的,因此人们通常简称它为 KMP 算法。可以证明它的时间复杂度为O(m+n),直观地看,是因为在匹配过程中指针 i 没有回溯。KMP算法的核心思想是利用已经得到的部分匹配信息来进行后面的匹配过程。
一般匹配算法中,进行"两个子串的比较"过程中,不管是T串的第几个字符发生"不等"的情况,指针i都必须 "回溯" 到S串中原来开始进行比较的字符的下一个位置。显然,算法最坏情况下的时间复杂度为O(m *n)。
大家对此有什么想法呢?难道每次都必须比较失配回溯到“S串中下一个位置”和“T串的起始位置”吗?
其实在KMP匹配过程中,当发生两个串中字符比较不等时指针 i 不需要回溯,而只要将 T 串向右滑动到一定位置继续进行字符间的比较。
例如按此算法进行模式串T = "abcac" 和主串S ="ababcabcabcacabca",S串中除了第3、7和10个字符和T串中的字符比较了两次之外,其它字符和T串中的字符均只进行了一次比较。
你知道是因为什么原因吗?
以S6!=t4而失配的情况为例,因为已经得到的匹配信息是:S2=t0,S3=t1,S4=t2,S5=t3,S6!=t4,而从T串本身得到的信息是:t1!=t0,t2!=t0,t3=t0,这就是说,不可能存在一个从 S2或S3或S4开始的子串和T串相等,但可能存在从S5开始的子串和T串相等,并且由于已经得到的" S5=t3,t3=t0"信息就不需要再进行S5和t0的比较,而直接进行S6和t1的比较即可。
那么如何才能迅速的判断主串S中失配的字符应该和模式串T中的哪个字符继续开始比较呢?
其实,模式串T向右移动的位置只和模式串T以及失配字符在模式串T中的位置有关,所以对于每个模式串T在匹配之前便可以计算出。KMP算法中,引入了next函数,其含义是:当出现 Si!=tj时,下一次的比较应该在Si和tnext[j]之间进行,如下例:
j |
0 |
1 |
2 |
3 |
4 |
模式串 |
a |
b |
c |
a |
c |
next[j] |
-1 |
0 |
0 |
0 |
1 |
下一个问题是如何求模式串的next函数值?
求next函数的过程是一个递推的过程:
1.首先由定义得next[0]=-1,next[1]=0;
2.假设已知next[j]=k,又T[j] = T[k],则显然有next[j+1]=k+1;
3.如果T[j]!= T[k],则令k=next[k],直至T[j]等于T[k]为止。
注:
1.虽然next定义中没有明确指出next[1]=0,但由0<k<j的条件很容易判断出next[1]只能等于0;
2.next[j]=k表明在T串中的字符T[k]之前存在一个长度最大的子串"tj-ktj-k+1…tj-1"和T串中的子串 "t0t1…tk-1" 相等,而现在又知道了tj=tk,这就是说,在字符T[k+1]之前存在着一个长度最大的子串使得等式"t0t1…tk"="tj-ktj-k+1…tj"成立,则根据next函数值的定义不就得到next[j+1]=k+1了吗?
3.由于tj!=tk,则等式"t0t1…tk"="tj-ktj-k+1…tj" 不成立,也就是说,在字符T[k+1]之前不存在一个子串" tj-ktj-k+1…tj"和子串"t0t1…tk"相等,那么是否可能存在另一个值p<k,使等式"t0t1…tp"=" tj-ptj-p+1…tj" 成立,这个p显然应该是 next[k],因为这相当于一个"利用next函数值进行T串和T串的匹配"问题。
由此可得下列求 next 函数值的算法,它和上述的KMP算法非常相似。
void get_next(char T[], int next[])
{
// 求模式串T的next函数值并存入数组 next。
j = 0; next[0] = -1; k = -1;
while ( T[j+1] != '\0' ) {
if (k = = -1 || T[j] = = T[k])
{ ++j; ++k; next[j] = k; }
else k = next[k];
}
} // get_next
因为在S[i]和T[j]不等时,由于T[j]=T[k],则S[i]肯定也不等于T[k],也就是说,S[i]不应该再去和T[k]进行比较(因为纯粹是多余的),而应该直接和T[next[k]]进行比较。因此,当T[j]=T[k]时,应该直接取T[k]的next函数值作为T[j]的 next 函数值。
void get_nextval(char T[], int next[])
{
// 求模式串T的next函数值并存入数组 next。
j = 0; next[0] = -1; k = -1;
while ( T[j+1] != '\0' ) {
if (k = = -1 || T[j] = = T[k]) {
++j; ++k;
if (T[j]!=T[k]) next[j] = k;
else next[j] = next[k];
} // if
else k = next[k];
} // while
} // get_nextval
Reference:《数据结构》严蔚敏