KMP算法
在介绍KMP算法之前先介绍一下BF算法(暴力破解算法)。
BF算法
假设有两个字符串,一个叫做主串,另一个叫做模式串。现在想要知道模式串在主串的什么位置。那就要进行两个字符串的比较,我们一般想到的办法定义两个标记 i,j,让i指向主串的第一个字符,j指向模式串的第一个字符然后通过移动i,j的位置逐位比较,当模式串没有匹配成功,就要将i和j进行回退重新比较,j回退到模式串首位,i回退到比较起始位置的下一位。
如下图:当比较到图2的时候会发现D≠C,所以要进行i和j回退,回退到图3继续比较,直到在主串匹配到模式串或比较到主串的最后一位。
附上代码
static int BF(String str1,String str2){
char ch1[] = str1.toCharArray();
char ch2[] = str2.toCharArray();
int i=0,j=0;
while (i < ch1.length && j < ch2.length){
if (ch1[i] == ch2[j]){
i++;
j++;
}else {
j = 0;
//回退到本次起始比较位置的下一位
i = i-j+1;
}
}
if (j == ch2.length){
//返回模式串在主串的起始位置
return i-j;
}
return -1;
}
这就是BF算法的思路,这种思路虽然可以得到最终的结果,但是时间复杂度最坏情况是O(mn),那么有没有时间复杂度更少的算法?
Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表了KMP算法,该算法时间复杂度O(m+n)。KMP算法的强大之处就是当遇到字符串不匹配时只需移动模式串的标记,并且模式串中标记的移动还是有技巧的------next数组,而不是直接移动到首位。
next数组
我们假设next数组和模式串的下标都是0开始,它们一一对应,因为数组下标我们习惯从0开始。
next数组的作用:next[ j ] = k这个式子表示当模式串中下标为j的字符与主串不匹配时,模式串的标记应该移到下标为k的位置继续与主串比较,所以这个k也代表了模式串下标j之前字符串的前后缀最大重复长度数值。
下标为k/next[j]的位置数值 == 前后缀最大重复长度数值 ==重复串的下一位的数值(下图中的 1 ==1 == 1)
- 前后缀:前缀就是字符串中除了最后一位其它字符的各种顺序组合,如字符串abcab中最后一位字符b它的前缀就是(a、ab、abc、abca)。同理后缀就是(b、ab、cab、bcab)。
下面举个例子来说明某字符不匹配时,标记要移动到的位置数值与其前后缀最大重复长度数值的关系。
如下图模式串是ABCABE,当字符串最后一位E不匹配时,应将模式串中的标记移动到模式串的那个位置呢?
首先我们要算出E之前字符串ABCAB中的最大前后缀重复长度,很明显是2,因为重复的串是AB。
前缀:A AB ABC ABCA ABCAB 后缀:B AB CAB BCAB ——>最大重复串是AB。
当比较到E的时候说明模式串E之前的位置都和主串匹配成功,我们可以求出相同的前后缀,如果我们直接用前缀那部分替代后缀(见下图红圈)也是和主串匹配成功的,就不需要从首位在进行比较,所以直接移到前后缀最大重复串的下一位继续比较,这样效率大大提高。
所以当E不匹配时我们要将标记移动到模式串的下标为2的位置(字符C的位置),主串中标记不用移动继续匹配。
- next数组中内容就是模式串每一位字符不匹配时要移动到位置的下标。
有了next数组,当不匹配时就不用移动主串的标记,模式串按照next数组中的值移动,从而提高效率。
那么next数组到底怎么求解呢?
先给出代码如下:
public static int[] getNext(String str){
char ch[] = str.toCharArray();
int next[] = new int[str.length()];
int k = -1;
int j = 0;
next[0] = -1;//当下标为0的位置不匹配没有办法向左移动,所以赋值-1.
while (j < str.length()-1){
if (k==-1 || ch[k] == ch[j]){
next[++j] = ++k;
}else {
//这段代码最有难度的一句代码
//如果只说起到回退作用可能很不好理解,下面进行分析
k = next[k];
}
}
return next;
}
next数组求解分析:
当下标为为0时,如果不匹配
此时标记已经在最左边了没有办法移动,所以将next[0] = -1;
当下标为1的位置不匹配
此时它前面只有一个字符,没有前后缀,所以只能移动到下标为0的位置。next[1]=0; 上述代码中第一步就是next[++0] = ++(-1) ——> next[1]=0。
注:next数组前两位值是固定的。
当匹配到下标为2以后就会有不同情况:
当P[j] =P[next[ j ] ]时,next[j+1] = next[j] + 1;
如下图 标记 j 位置的字符C,我们令P[ j=5 ]=C,而next[ j ] = 2,当P[ j ]=P[ next [j] ]——> P[5] = P[2]时,next[6]=3=next[2]+1;
这很好理解因为在之前基础上多了一位重复字符,相应的next数组值也就是之前的next值加1。
当P[j] ≠ P[next[ j ] ]时,在代码中会进行 k = next[k]处理;那么这句代码到底是什么意思呢?**
在下图中最后一位D和E不匹配,此时 P[j]=F ≠ P[next[j]]=B,如果还按照P[j+1]不匹配,next[j+1] = next[j]+1,那么就会出现如下图红色字的位置不匹配,但是我们从字符C开始与主串比较,明显是错误的。
所以到底应该怎么做呢?
此时应继续向前找位置——次最大前后缀从重复长度,详情如下。
首先我们要明白,当next[j] = k时,说明[0~~k-1] 与[j-k ~j-1]是重复的,如下图紫色箭头所指。因为前面已经说过next数组存的值就是前后缀最大重复长度(不匹配时要移动到的位置下标)。
当进行j+1不匹配时,我们如果仍然按照 next[j+1]=next[j]+1 将匹配位置移动到图中 j-k-1(next[j]+1)的位置直接比较,因为P[j] ≠ P[next[ j ] ],显然错误(上面已经解释过)。
所以我们当然要找次大的重复串,因为重复串长度越大,下次比较时不用比较的字符就越多,效率也就越高。找到次大重复串,然后判断次大重复串的下一位(P[next[k]])是否与P[j]位置相同,不相同继续找下一个次大重复串 ,相同则可以将next[j+1]的值设置为重复串长度+1(重复串的长度就是重复串的下一位的下标值)。
下面结合代码分析:
while (j < str.length()-1){
if (k==-1 || ch[k] == ch[j]){//比较ch[k]与ch[j] 等价于P[next[j]]比较P[j]
next[++j] = ++k;
}else {
k = next[k];//把次大重复串的下一位的位置赋值给k,
//while后又进入到if中
// 此时k的值为next[next[j]],
//进行ch[k]比较ch[j],然后再分两种情况处理
//不相等就像上面说的继续给k重新赋值查找;
//相等就在if中给next[j+1]赋值。
}
}
P[j] ≠ P[next[ j ] ],所以在下图中我们已经找了next[k],红色两部分是相同的(在next[k]的两端),因为紫色箭头两部分是相同的,所以能证明绿色部分也是相同的,这四部分都是相同的,所以每一小部分都是次最大前后缀重复串。当图中j+1不匹配时,首先看j和next[j]是否相同,不相同说明要在[j-k ~ j-1] =[0 ~ k-1]中找次大重复串,因为四部分相等的就是次大重复串所以要把next[j]移动到next[k],也就是代码中 k = next[k]。
直到将next数组全部算出,至此就是next数组求解的分析。最好自己在debug模式把代码调试下更好理解。
next数组的优化:
如下图当出现如下不匹配的情况,将标记移动到next[j]时也不会匹配,因为在后面的B不匹配,移动到前面的B肯定也不匹配,所以这一步没意义,问题出在P[j]=P[next[j],应该跳过next[j]继续前移到next[next[ j ]],再看是否还会出现这种情况,一直前移到没有这种情况,然后在比较,否则一直跳过。
所以在代码中只需要加上一步判断,当前j位置所不匹配的字符和next[j]位置的字符是否相同,相同则跳过。
代码
public static int[] getNext(String str){
char ch[] = str.toCharArray();
int next[] = new int[str.length()];
int k = -1;
int j = 0;
next[0] = -1;//当下标为0的位置不匹配没有办法向左移动,所以赋值-1.
while (j < str.length()-1){
if (k==-1 || ch[k] == ch[j]){
//当比较字符不匹配,将要移动到那个位置上的字符与比较字符相同时
//说明移动到那个位置也还是不匹配此时可以跳过继续向前找。
if(ch[++k]==ch[++j]){
next[j] = next[k];
}else{
next[j] = k;
}
}else {
//这段代码最有难度的一句代码
//如果只说起到回退作用可能很不好理解,下面进行分析
k = next[k];
}
}
return next;
}
完整KMP代码
static int KMP(String str1,String str2){
char ch1[] = str1.toCharArray();
char ch2[] = str2.toCharArray();
int next[] = getNext(ch2);
int i=0,j=0;
while (i < ch1.length && j < ch2.length){
if (ch1[i] == ch2[j]){
i++;
j++;
}else {
j = next[j];
}
}
if (j == ch2.length){
return i-j;
}
return -1;
}
static int[] getNext(char chh[]){
char ch[] = chh;
int next[] = new int[ch.length];
int k = -1;
int j = 0;
next[0] = -1;//当下标为0的位置不匹配没有办法向左移动,所以赋值-1.
while (j < ch.length-1){
if (k==-1 || ch[k] == ch[j]){
if(ch[++k]==ch[++j]){
next[j] = next[k];
}else{
next[j] = k;
}
}else {
//这段代码最有难度的一句代码
//如果只说起到回退作用可能很不好理解,下面进行分析
k = next[k];
}
}
return next;
}