初时KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。
基础前提
1.前缀后缀最长公共元素长度
最长前缀:以第一个字符开始的子串(但是不包括最后一个字符)。
最长后缀:从最后一个字符结束的子串(但是不包括第一个字符)。
最长公共元素就是,前缀与后缀完全匹配时,包含的最多的字符数。
举例:字符串“ababc",其各个子串的最长公共元素长度如下表所示:
字符串 | a | b | a | b | c |
最大前缀后缀公共元素长度 | 0 | 0 | 1 | 2 | 0 |
2.next数组
next数组考虑的是,除当前字符外的最长相同前缀后缀,所以,可以看出其与最长公共元素长度存在关系。
规则为:将“ababc"对应的最长公共元素长度表的数值,依次向右移动一位,然后赋初值为-1。得到下表:
字符串 | a | b | a | b | c |
最大前缀后缀公共元素长度 | 0 | 0 | 1 | 2 | 0 |
next数组 | -1 | 0 | 0 | 1 | 2 |
next数组作用
KMP的next数组,相当于告诉我们,当模式串中的某个字符跟源文本串中的某个字符匹配失败时,模式串下一步应该跳到哪个位置。如模式串中在j处的字符跟文本串在i处的字符不匹配,下一步就用,next[j]处的字符继续跟文本串i处的字符匹配(即j=next[j],相当于模式串向右移动了j-next[j]位)。图示分析如下:
如何求得next数组
基于之前的理解,可知计算next数组最好的方法就是,采用递推形式。
规则如下:其中k和j都是模式串中的两个下标值(k<j),已知求得了next[j]=k,如何就next[j+1]???
1.若p[k] == p[j],则next[j+1] = next[j]+1 = k+1;
2.若p[k] != p[j],如果此时p[next[k]] == p[j],则next[j+1] = next[k]+1;否则,继续递归前缀索引 k=next[k],而后重复此过程。
递归求next数组,代码如下:
void GetNext(char []p, int []next){
int pLen = p.length;
next[0] = -1;
int k = -1;
int j = 0;
while(j < pLen-1){
//p[k]表示前缀,p[j]表示后缀
if(k==-1 || p[j]==p[k]){
++k;
++j;
next[j] = k;
}else{
k = next[k];
}
}
}
KMP算法过程
next数组是KMP的核心,上面已经得到了next数组。KMP的规则如下:当前源文本串S匹配到i位置,模式串P匹配到j位置
1.如果j == -1,或者当前字符串匹配成功(即S[i] == P[j]),则令i++,j++,继续匹配下一个字符;
2.如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令i不变,j = next[j]。意味着失配时,模式串P相对于文本串S向右移动了 j-next[j]位(即:失配字符所在位置 - 失配字符对应的next值,且此值大于等于1)。【这里是KMP算法的关键之处,以此避免了失配时,S中字符回溯产生的重复匹配过程】。
KMP代码过程如下:
int Kmp(char []s, char[]p){
int sLen = s.length;
int pLen = p.length;
int i = 0;
int j = 0;
while(i<sLen && j<pLen){
if(j==-1 || s[i]==p[j]){//匹配成功
i++;
j++;
}else{//匹配失败
j = next[j];
}
}
if(j == pLen)
return i-j;
else
return -1;
}