KMP
定义
Knuth-Morris-Pratt 字符串查找算法(常简称为 “KMP算法”)是在一个文本串 S内查找模式串T 的出现,通过观察发现,在不匹配发生的时候这个词自身包含足够的信息来确定下一个匹配将在哪里开始,以此避免对以前匹配过的字符重新检查。通俗点说就是一种在一个字符串中定位另一个串的高效算法,它的时间复杂度为O(m+n)。
原理
分析
在此,给出一个KMP字符串匹配的实例(网上找的):
如上图所示,在S=”abcabcabdabba”中查找T=”abcabd”,如果使用KMP匹配算法,当第一次搜索到S[5] 和T[5]不等后,S下标不是回溯到1,T下标也不是回溯到开始,而是根据T中T[5]==’d’的模式函数值(next[5]=2,为什么?后面讲),直接比较S[5] 和T[2]是否相等,因为相等,S和T的下标同时增加;因为又相等,S和T的下标又同时增加。最终在S中找到了T。如图:
KMP匹配算法和简单匹配算法效率比较,一个极端的例子是:在S=“AAAAAA…AAB“(100个A)中查找T=”AAAAAAAAAB”, 简单匹配算法每次都是比较到T的结尾,发现字符不同,然后T的下标回溯到开始,S的下标也要回溯相同长度后增1,继续比较。如果使用KMP匹配算法,就不必回溯。
对于一般文稿中串的匹配,简单匹配算法的时间复杂度可降为O (m+n),因此在多数的实际应用场合下被应用。
KMP算法的核心思想是利用已经得到的部分匹配信息来进行后面的匹配过程。看前面的例子。为什么T[5]==’d’的模式函数值等于2(next[5]=2),其实这个2表示T[5]==’d’的前面有2个字符和开始的两个字符相同,且T[5]==’d’不等于开始的两个字符之后的第三个字符(T[2]=’c’)。如图:
也就是说,如果开始的两个字符之后的第三个字符也为’d’,那么,尽管T[5]==’d’的前面有2个字符和开始的两个字符相同,T[5]==’d’的模式函数值也不为2,而是为0。
前面说过:在S=”abcabcabdabba”中查找T=”abcabd”,如果使用KMP匹配算法,当第一次搜索到S[5] 和T[5]不等后,S下标不是回溯到1,T下标也不是回溯到开始,而是根据T中T[5]==’d’的模式函数值,直接比较S[5] 和T[2]是否相等。为什么可以这样?
刚才提到过next[5]=2,其实这个2表示T[5]==’d’的前面有2个字符和开始的两个字符相同,如上图所示,因为,S[4] ==T[4],S[3] ==T[3],根据next[5]=2,有T[3]==T[0],T[4] ==T[1],所以S[3]==T[0],S[4] ==T[1](两对相当于间接比较过了),因此,接下来比较S[5] 和T[2]是否相等。
有人可能会问:S[3]和T[0],S[4] 和T[1]是根据next[5]=2间接比较相等,那S[1]和T[0],S[2] 和T[0]之间又是怎么跳过,可以不比较呢?因为S[0]=T[0],S[1]=T[1],S[2]=T[2],而T[0] != T[1], T[1] != T[2],==> S[0] != S[1],S[1] != S[2],所以S[1] != T[0],S[2] != T[0]. 还是从理论上间接比较了。
有人疑问又来了,你分析的是不是特殊轻况啊。
假设S不变,在S中搜索T=“abaabd”呢?答:这种情况,当比较到S[2]和T[2]时,发现不等,就去看next[2]的值,next[2]=-1,意思是S[2]已经和T[0]间接比较过了,不相等,接下来去比较S[3]和T[0]吧。
假设S不变,在S中搜索T=“abbabd”呢?答:这种情况当比较到S[2]和T[2]时,发现不等,就去看next[2]的值,next[2]=0,意思是S[2]已经和T[2]比较过了,不相等,接下来去比较S[2]和T[0]吧。
假设S=”abaabcabdabba”在S中搜索T=“abaabd”呢?答:这种情况当比较到S[5]和T[5]时,发现不等,就去看next[5]的值,next[5]=2,意思是前面的比较过了,其中,S[5]的前面有两个字符和T的开始两个相等,接下来去比较S[5]和T[2]吧。
next证明
现做以约定,假设S为文本串,T为模式串,其中S中字符用s+数字表示,即s1代表S中的一个字符,若文本串S为"s0s1s2s3",则S由s0、s1、s2、s3四个字符按此顺序组成的字符串。同理,T中字符用t+数字表示。
假设S为"s0s1s2....sn",T为"t0t1t2...tn",那么在匹配当中需要解决的问题则是当S与T在匹配的过程中如果在S的第i个字符与T的第j个字符处失配的时候,接下来S串第i个字符应与T中的哪个字符进行比较?
若现在应与T中第k(k<j)个字符进行比较,那么必有以下关系式成立:
"t0t1t2...t(k-1)" = "s(i-k)s(i-k+1)s(i-k+2)...s(i-1)"
而已经得到的“部分匹配”的结果是:
"t(j-k)t(j-k+1)t(j-k+2)...t(j-1)" = "s(i-k)s(i-k+1)s(i-k+2)...s(i-1)"
由上面两个等式我们可以推出等式如下:
"t0t1t2...t(k-1)" = "t(j-k)t(j-k+1)t(j-k+2)...t(j-1)"
综上所述,next值得实质含义则是在匹配在第j个字符前面子串中前缀与后缀相同的长度,若以下标零开始,那么也代表着若失配后T串应该回溯到哪个位置与T串的第i个字符进行比较。
核心代码
到此,问题转化为了求T串自身在当前位置(即与S串正在匹配位置)若与S串失配需要回溯到的位置,源码如下:
1 void GetNext(char* T,int len) 2 { 3 int i,j; 4 5 next[0] = 0; 6 next[1] = 0; 7 8 for(i = 1; i < len; i++) 9 { 10 j = next[i]; 11 while(j && T[i] != T[j]) 12 { 13 j = next[j]; 14 } 15 16 if(T[i] == T[j]) 17 { 18 next[i+1] = j+1; 19 }else 20 { 21 next[i+1] = 0; 22 } 23 } 24 }
其实在上述求得next的代码中,next[0]位置大可设为-1,这样也可以作为是首字母的一个标志,虽然在使用上没有什么大的区别,但是有时候却在编码中却能让其他地方的编码方便许多,而不至于苦恼于“有没有回溯到首字母”这个问题。
在此基础,还需根据上述分析对文本串与模式串进行匹配,源码如下:
1 int KMP(char* S,char* T) 2 3 { 4 5 int sLen,tLen; 6 7 int i,j; 8 9 int cnt; 10 11 sLen = strlen(S); 12 13 tLen = strlen(T); 14 15 GetNext(T,tLen); 16 17 18 19 cnt = 0; 20 21 j = 0; 22 23 for(i = 0 ; i < sLen; i++) 24 25 { 26 27 while(j && S[i] != T[j]) 28 29 { 30 31 j = next[j]; 32 33 } 34 35 36 37 if(S[i] == T[j]) 38 39 { 40 41 j++; 42 43 } 44 45 46 47 if(j == tLen) 48 49 cnt++;//T串在S串中出现次数 50 51 } 52 53 54 55 return cnt; 56 57 }
在KMP算法的源码中表达的意思是模式串在文本串中出现的次数,若想获得模式串在文本串中出现的首位置,相信如果真正理解KMP算法的本质,那么是不难办到的。
参考资料:
1.数据结构(严蔚敏 吴伟民编著)
2.部分内容来自于网络来自CSDN A_B_C_ABC 网友,由于没能找到原版网址,当时参考的转载网址为http://www.cppblog.com/oosky/archive/2006/07/06/9486.html
3.算法竞赛入门经典(刘汝佳 陈锋编著)
4.维基百科,百度百科。
由于个人对KMP算法有点晕晕的,所以搜集了些许资料并加上个人理解做了一些总结,在自学的过程中希望留下一手以后可以复习的资料,同时也希望可以帮助到与我一样对KMP不解的人。有什么不对的,还希望批评指正。若其中部分内容的原创者有觉不妥可在我的个人博客www.heweiyou.com中留言,我会尽快进行处理的。