KMP算法简单说是一种字符串匹配的算法,文本在这里详细研究一下KMP算法,也是初学,不妥之处望多多指教。
既然说到字符串匹配,肯定有两个字符串,一个作为源串,一个作为模板串,一般来讲源串比较长,模板串比较短,匹配算法就是用短的模板串去检测源串中是否存在和它相同的版本。
举个例子 我们设源字符串为Source string用数组s[]来表示,而模板字符串用数组T[]来表示。
假设s[]={a,d,f,e,a,d,a,f,c,s,h} 而T[]={a,d,a,f}.显然由于是故意设置的,这两个数组是存在匹配关系的,也就是从s[4]开始。
用肉眼看到的东西如何用程序来表示出来。先看看传统的匹配算法。
1. 传统的匹配算法
传统的匹配算法比较简单,就是遍历数组,从第一个字符开始(当然这里也可以指定开始位置),依次遍历并与模板数组比较,如果 匹配成功则继续下一组。否则回退到第二个字符串再次进行匹配。
用C++实现一下
int Simple(char s[],char T[]) { int pos=0; int j=0; while(T[j]!='\0'&&s[pos+j]!='\0') { if(s[pos+j]==T[j]) j++; else { pos++; j=0; } } if(T[j]=='\0') { printf("匹配成功\n"); return pos; } else return 0; }
传统算法比较简单,但缺点就是每匹配一次之后,要接着从下一个字符开始再次进行比较。比如开始的那个例子。
a,d,f,e,a,d,a,f,c,s,h
a,d,a,f
当第一次匹配结束之后,显然adfe不等于adaf,于是在传统的算法中pos会自动+1然后,从d处开始下一次匹配,尽管我们知道,下一次匹配也肯定不会成功,这就造成了回溯的冗余问题,也就是多次不必要的匹配。KMP算法可以看做是在普通算法的基础上进行了一定的优化,解决回溯冗余问题。
2.KMP算法
首先了解一下前缀,不用复杂的术语,简单讲就是包含从第一个字符开始,长度小于原字符的子字符串,比如上例中的T[]数组的前缀可以是ad,也可以是ada,当然也可以是a,相同的意思后缀也很好理解。
所谓的优化就是尽可能的减少匹配的次数,比如在普通算法中假设s[]数组的长度为n,T[]数组的长度为m,则时间复杂度是o(mn),说的通俗点就是每次要往后移一个字符,而对于KMP算法来说,就是每次通过前次的匹配经验尽可能的在不遗漏重要比较的情况下多移动几个字符。
还是拿这两个数组做例子第一次比较,在a≠f时比较结束。我们把已经匹配成功的个数定义为q,此处q=2,我们容易知道的是在数组T[]前缀ad中a≠d,而d=s[1],所以a≠s[1];也就是说a根本不需要跟s[1]进行比较而直接跳过它。
我们再定义一个整数L,L表示当前已匹配字符串的真自身前缀=真自身后缀的长度。比如本例中已匹配字符串为ad,真自身前缀为a,真自身后缀为d,a≠d,则L=0;所以求出移动的距离s=q-L=2。
为表明算法的通用性我们换一个复杂一点的字符串假设s[]="adadacadadabd" ,模板字符串T[]="adadab"。
a d a d a c a d a d a b d
a d a d a b
第一次匹配成功的字符串部分为adada,按照我们前边的定义q=5,真自身前缀=真自身后缀=ada,则L=3,于是移动的长度s=q-L=2;也就是说下次移动到s[2]处开始新一轮的匹配。
也就是所谓的next函数;
那么这个所谓的真自身前缀=真自身后缀是怎么来的。我们已经假设它相等了,当然L=0就不存在相等了。我们已知T[0]=T[2]=a,又因为这个前缀是匹配的,所以T[2]=s[2],所以我们可以得出T[0]=s[2]。跳过了T[0]和s[1]的比较。
所以我们得到的结论就是s=q-L是成立的。显然在这里q与L是一一对应的,不同的匹配字符串q都会有一个L与它对应,比如前面例子中的q=2时L=0,q=5时L=3。既然是一一对应的关系,不妨定义一个数组来表示这个对应关系,设其为Next[],数组下标表示q,下标所存放的值为L,比如Next[2]=0,也就是说当q=2时L=0;
怎么通用的求出这个数组,定义一个函数prefunc()来求这个Next[]数组。
函数体如下
void prefunc( char T[], int Next[] ) { int T_Len = 0; //获得数组T的长度,因为可匹配字符串q是不可能超过这个长度的。 while( '\0' != T[T_Len] ) T_Len++; int L = 0; // L也就是真自身前缀=真自身后缀时的长度 Next[1] = 0;//当q等于1时,L必等于0 for( int q=2; q<T_Len+1; q++ ) { while( L>0 && (T[L] != T[q-1]) ) //真自身前缀不等于后缀的情况 L = Next[L]; //显然此时的L不变,找不到新的真自身前缀等于真自身后缀 if( T[L] == T[q-1] )//如果找到新的则L++ L++; Next[q] = L; //最后存入数组 } }
pre函数的目的就是将L存放到以p为下标的数组Next中。
接下来就KMP算法的函数实现部分了。
void KMMatching( char s[], char T[] ) { int Pre[10]; int s_Len = 0; int T_Len = 0; // Compute the length of array Target and Pattern while( '\0' != T[T_Len] ) T_Len++; while( '\0' != s[s_Len] ) s_Len++; // Compute the prefix function of Pattern prefunc(T,Pre ); int q= 0; // Number of characters matched for( int i=0; i<s_Len; i++ ) { while( q>0 && T[q] != s[i] ) q = Pre[q]; if( T[q] == s[i] ) q++; if( q == T_Len ) { cout<<"KMP String Matching,pattern occurs with shift "<<i -T_Len + 1<<endl; q = Pre[q]; } } }