一.KMP算法概念
来看上图,当比较串与模板串比较到AB处不相同时,原始做法是模板串前进一个单位,比较串回溯到开始重新比较,这样做效率低下,很多重复部分。我们要明确一点:模板串与比较串此时的1-2和3-4部分是相同的!所以如果此时串中开头与结尾的一部分是对称相同的,我们可以直接把比较串移动到C处与A比较(而模板串不回溯),这样就能提高效率!
充分利用了目标字符串ptr的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量)。
以下关系递推式转自:点击打开链接
假设主串:S: S[1] S[2] S[3] ……S[n]
模式串:T: T[1] T[2] T[3]…..T[m]
现在我们假设主串第i 个字符与模式串的第j(j<=m)个字符‘失配’后,主串第i 个字符与模式串的第k(k<j)个字符继续比较,此时就有S[i] != T[j]
主串: S[1]...S[i-j+1]...S[i-1]S[i]...
||(匹配) || ≠
模式串: T[1]... T[j-1] T[j]
由此,可以得到关系式如下
T[1]T[2]T[3]...T[j-1] = S[i-j+1]...S[i-1]
由于S[i] != T[j],接下来S[i]将与T[k]继续比较,则模式串中的前k-1咯字符串必须满足下列关系式,并且不可能存在k'>k满足下列关系式:
T[1]T[2]T[3]...T[k-1] = S[j-k+1]S[j-k+2]...S[i-1] (k<j)
也就是说:
主串: S[1]...S[i-k+1]S[i-k+2]...S[i-1]S[i]...
|| || || ?(待比较)
模式串: T[1] T[2]... T[k-1] T[k]
现在可以把前面的关系综合总结如下:
S[1]...S[i-j+1]...S[i-k+1]S[i-k+2]...S[i-1]S[i]...
|| || || || ≠
T[1]... T[j-k+1] T[j-k+2]... T[j-1] T[j]
|| || || ?
T[1] T[2] ... T[k-1] T[k]
现在唯一的任务就是如何求k了,通过一个next函数求。二.KMP算法前期准备
1.最长前缀概念: 最长前缀是说以第一个字符开始,但是不包含最后一个字符。
2.最长后缀概念: 最长前缀是说以最后一个字符开始,但是不包含第一个字符。
3.next[]数组:保存每一个字符处的最大最长前缀与最长后缀相等长度,即图中的len(3=4), 用于比较时移动和回溯,目的是实现最大移动量!(其实next[i]表示的是1~i位置中最长的前后缀相同长度,他的值一方面表示长度,另一方面表示这一最大的前缀末坐标。)
4.KMP算法:利用next数组实现比较串的移动与回溯,模板串不回溯。
三.如何求next数组
1.已知:next数组的值是最大的相同前缀与后缀长度,也可以表示这个最大长度表示的前缀末座标。若现在已知next[j],如果我们比较S[next[j]+1]==S[j+1](前缀后缀同时后移一个单位比较),则就表示在原来基础上最大相同前后缀又多了一:next[j+1]=next[j]+1;
2.若S[next[j]+1]!=S[j+1]:
即A!=B,此时应该在A之前继续找BF段的相同最大前缀,我们可以直接k--寻找,但是我们可以直接回溯i=next[i],找他之前的最大前缀末坐标,如果此时S[i]==S[j+1];则next[j+1]=next[i]+1;证明:因为此时B=C末坐标,而之前有CD=EF,是对称的!所以F=C-1,而B=C末即C=BF!出现最大前后缀。
综上:
- M[k] == M[i] 此时 next(i+1)=k+1=next(i)+1
- M[k] ≠ M[i] 此时只能在M位置k之前的字符串中匹配,假设j=next(k),如果此时M[j]==M[i],则next(i+1)=j+1,否则重复此步骤,直到字符串长度为0为止
代码:
int next[1000]; void getNext(string s,int l)//这里next是从0开始的,与字符串坐标一一对应,表示该0~下标处的最大长度。-1表示没有,0表示1,1表示2,以此类推! { next[0]=-1; //所以回溯的时候,回溯到的是最大前缀的末坐标k,而我们要比较的是k+1与j+1,所以坐标要后移一位于j+1比较,出现了s[k+1] int k=-1; //但是next等于的还是最大后缀的末下标,与字符串下标对应。 for(int i=1;i<=l-1;i++) { while(k>-1&&s[k+1]!=s[i]) k=next[k];//回溯 if(s[k+1]==s[i]) k=k+1;//next[j+1]=next[j]+1; next[i]=k; } }
int next[1000]; int getNext(char *s,int l)//这个版本,next数组是从1开始的,字符串是从0开始的,这里next 0就表示没有,数字表示长度 { int j=0; //所以回溯的时候,回溯到的比如长度是2,在字符串里就是第三个字符,自动跳到了最大前缀末坐标的下一个,不用k+1了 int k=-1; next[0]=-1; while(j<l) { if(k==-1||s[j]==s[k]) next[++j]=++k;//next[j+1]=next[j]+1; else k=next[k];//回溯 } }
4.KMP算法模板(求第一次出现位置+出现次数)
void getNext(string s,int l) { next[0]=-1; int k=-1; for(int i=1;i<=l-1;i++) { while(k>-1&&s[k+1]!=s[i]) k=next[k]; if(s[k+1]==s[i]) k=k+1; next[i]=k; } } int KMP(string s1,int l1,string s2,int l2) { int k=-1; for(int i=0;i<l1;i++) { while(k>-1&&s2[k+1]!=s1[i]) k=next[k]; if(s2[k+1]==s1[i]) k=k+1; if(k==l2-1) return i-l2+1; } return -1; } void KMP(char *str,int l1,char *s,int l2) { int k=-1; for(int i=0;i<l1;i++) { while(k>-1&&s[k+1]!=str[i]) k=next[k]; if(s[k+1]==str[i]) k=k+1; if(k==l2-1) { sum++; k=next[k]; } } }2. 点击打开链接