网上关于KMP的讲解已经够多了,但我感觉很多的文章对于一些关键点的解释还不够清晰,如果你还不知道KMP算法,那建议你先百度了解一番KMP,如果了解完后感觉大脑还是塞塞的,思路不够清晰的话再来看看我这篇文章。这里就不再对KMP从头到尾讲述了。毫无疑问,KMP的关键点就是求next数组,我只针对如下两点做解释以及给与数学证明。
针对字符串str求它的next数组:
1 next[i]的意义:
next[i]的意义是当下标为i的元素不匹配时,它的前缀和后缀能够匹配的最长长度,计算前缀和后缀的时候是不把当前字符考虑进去的,因为我是当前字符失配时跳转到下一个位置(也就是说这个字符之前的字符都匹配了),以及计算前缀时不能把最后一个字符包括,计算后缀时当然也不能把第一个字符考虑进来,比如“AAABC”,当计算第四个字符B的时候,它的前缀和后缀匹配的最长长度是2,因为第一个和最后一个字符不能互相包括嘛。 所谓的前缀 和 后缀 都是从左到右计算的,也就是下标从小到大的,比如字符串“ABABB”,计算最后一个字符B的next值时,后缀为AB。
好了,理解了next[i]的意义后,我们说说next[0]和next[1]的值,这两个的值是确定的,next[0]=-1,next[1]=0。我来解释为什么这么设置它们的值。我们在在计算第J个字符的next值时,让i=J-1,如果str[ next[i] ] 与 str[ J-1 ]不匹配时,我们会进行跳转,也就是让i=next[i](待会会解释为什么这么跳转),但是我们需要一个终止条件,也就是前缀长度为1时也不匹配的时候,这时应该终止,而当第0个元素与str[J-1]不匹配时,此时恰好i=next[0],i=-1了,此时我们知道前后缀能匹配的最长长度为0,因此我们把next[0]设置为-1,当然设置为-2,-3都可以,因为它只是一个终止条件,好让我们判断i=该值时,匹配长度为0,可以计算下一个字符的next值了。
为什么next[1]=0呢,因为,next[1]之前只有一个字符,那个字符既是它的前缀也是后缀,而next数组的定义中已经说明了,计算的时候不能互相包括,所以next[1]直接设置为0,真正的next数组从2开始计算起。这里我给出当str [next[i]]==str[ J-1 ]时,next[ j ]=next[i]+1的数学证明:
采用反证法:
假设第J个字符存在更大的next值,设这个值为K,那么K>next[i]+1,则 K-1>next[ i ] ,(此处i=j-1) 根据next数组的求法我们知道,也就是存在某个值P和L,使得 str[0~P]=str[L~i],也就是存在str[0~P-1]=str[L~ i-1],这个值恰好是next[i]的值,等于K-1,而根据前置条件K-1>next[i],两者矛盾,故当str [next[i]]==str[ J-1 ]时,next[ j ]=next[i]+1。(原谅我写这么多文字而不带一张图,请耐下心来)
2 next[i]的跳转:
上面已经说了,让i=J-1,如果str[ next[i] ] 与 str[ J-1 ]不匹配时,我们会进行跳转,也就是让i=next[i],然后继续比较直到求得next[J]的值。(没错,在求next[J]的值时,每次都是与str[J]比较) 这么跳转的原因还得从next数组每个值的意义说起,就是前后缀匹配的最长长度。
next [ i ]的意义是存在两个值P,K使得str[ 0~P ]和str[ K ~ i-1 ]匹配(那么也就是next[i]=P+1)。
令i=J-1,假设我们的str[ next[i] ] 与 str[ J-1 ] 不匹配,
即 str[P+1]!=str[J-1],根据next数组的定义,我们现在要找两个新的值M,N来组成前缀和后缀,使得str[ 0 ~ M ]=str[ N ~ i-1 ]匹配再比较str [ M+1 ]与str [ i ]是否相等以此来求next[J]的值,我们知道新求得的匹配长度必然是小于next[i]的(待会会证明),也就是说M<P,N>k, 因为str [ 0~P ]与str [ k ~ i-1 ]相等,因此str [ N ~ i-1 ]是str [ 0 ~ P ]的一个子串,那么我们现在其实就是在求str[ P+1 ]也就是str[ next[i] ] 的next值。所以才让i=next[i].
现在我证明,新求得的匹配长度必然是小于next[i]的(注意这里我证明的是什么,不清楚的看上面那段话中,我说的待会会证明)。
反证法:
假设新求得的长度为L,且L>=next[i]。那么存在str [ 0~L-1 ]=str [ J-L+1 ~ i-1 ],而我们next [ i ]的值的定义就是第i个字符前的最长前后缀匹配长度,如果还存在新的长度L满足这种关系的话,那么不满足我们的next数组的定义,两者矛盾,因此命题得证。
本来很想画图,但苦于没有没有合适的画图工具,导致我只能纯文字的叙述,希望能够帮到对KMP尚还不清晰的朋友,另外,我的一贯原则是talk is cheap show me the code 所以 这里还是提供一个小例子,通过kmp来实现strstr函数
#include <iostream> using namespace std; //求next数组 void getNext(char* str,int* arr,int n){ arr[0]=-1; if(n<=1) return; arr[1]=0; int j=2,i; while(j<n){ i=j-1; while(1){ if(arr[i]==-1||str[j-1]==str[arr[i]]){ arr[j]=arr[i]+1; j++; break; }else i=arr[i]; } } } //统计字符串长度 int strlen(char* str){ int len=0; while(*str++!='\0') len++; return len; } //返回在源串中第一个与子串匹配的字符的起始地址,若没有匹配则返回NULL char* strstr(char* src,char* match){ int len1=strlen(src); int len2=strlen(match); if(len1==0||len2==0||len1<len2) return NULL; int* next=new int[len2]; getNext(match,next,len2); int j=0,i=0; while(i<len1&&j<len2){ if(j==-1||src[i]==match[j]) { i++; j++; } else j=next[j]; } delete[] next; if (j==len2) return (src+i-j); return NULL; } int main(int arg1,char** arg2){ char str1[]="today is sunny and all is right"; char str2[]=" al"; char* res=strstr(str1,str2); if(res!=NULL) cout<<res<<endl; else cout<<"res is null"<<endl; getchar(); }
假设我现在更改需求,需要返回所有与模式串匹配的下标呢(源串中的每个字符只能用一次,也就是说各个子串在源串中的位置不重叠)
look is cheap show me the code。
可以把下标都存在vector中,将其作为返回值,有兴趣的朋友可以写一写。
若有错误,或是建议,欢迎留言。
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:http://blog.csdn.net/u011408355/article/details/47981025