KMP算法分析以及java代码

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)

在这里插入图片描述

  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的位置),主串中标记不用移动继续匹配。

  1. 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;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值