KMP算法的具体计算过程

**解决字符串匹配问题
最长相等前后缀
前缀表next[]数组
j = next[j-1]**

一、解决字符串匹配问题

1. BF算法:模式串和主串,从头开始匹配,如果遇到不匹配,则模式串从头开始,主串返回到原来起始位置向后移动一位重新匹配。 在这里插入图片描述
BF算法很好理解但很“笨拙”,效率低下,时间复杂度为:O(m*n),m为字串长度,n为主串长度,空间复杂度较低为O(1)。

public static int forceMatch(char[] mainString,char[] pattern){
	  int i = 0;
	  int j = 0;
	  //回溯的指针
	  int k = 0;
	  while (i < mainString.length && j < pattern.length){
		    //继续匹配模式串后继字符
		    if (mainString[i] == pattern[j]){
		      i ++;
		      j ++;
		    }//模式串右移 回溯主串地址
		    else {
		      k++;
		      i = k;
		      j = 0;
		    }
	  }
	  if (j == pattern.length){
	    	return k;
	  }
	  return -1;

2. 快速匹配(KMP)算法:充分利用模式串和主串已经部分匹配的子串的最大相等前后缀这个信息,保持i指针不回溯,通过修改j指针,让模式串尽量的移动到有效的位置。这个解释比较绕口,这里我先把代码放上来,我们可以根据代码通过一些例子来帮助我们理解。

public static int strStr(String haystack, String needle){
        if(needle.length()==0){
            return 0;
        }
        int[] next = new int[needle.length()];
        getNext(next, needle);
        int j = 0;
        for(int i = 0; i<haystack.length();i++){
            while(j>0 && haystack.charAt(i) != needle.charAt(j)){
                j = next[j-1];
            }
            if(haystack.charAt(i)==needle.charAt(j)){
                j++;
            }
            if(j==needle.length()){
                return (i+1 - needle.length());
            }
        }
        return -1;
    }

举个例子如下图所示:

与BF算法一样,KMP算法也是从头开始匹配,第一次匹配直到:mainString[i] != pattern[j],这里我们肯定不希望像BF算法一样下一次匹配变成:
在这里插入图片描述
而是变成这样,i指针不动,j指针回溯。
在这里插入图片描述
可能会有人有疑问,为什么可以通过回溯来确定指针j的位置呢?看下图:
在这里插入图片描述
从上图已经匹配好的模式串的子串中我们可以发现:

  1. 模式串的子串①和②相等,并且根据前面的匹配必须有②和主串的③相等,由此可以推出:子串①和③相等。
  2. 在此基础上,固定指针i不动,j回溯到最长相等前后缀的位置即B的位置。子串AABAA的最长相等前后缀为2(可以直接根据后缀表快速得出),而B的位置刚好是pattern[2]=B,故j回溯后由j=5变成了j=2(这也就是为什么j=next[j-1],而不是j = j-1,后面我会通过后缀表再解释一遍)。
  3. i不动,依然等于5,再次判断mainString[i] 和 pattern[j](mainString[5] 和 pattern[2]是否相等。
  4. 若相等退出while循环,若不相等,j继续回溯,(这也就是为什么用while循环而不是if判断的原因)上述例子刚好是mainString[5] = pattern[2],退出while循环,j++,i++;继续匹配后面的字符。

二、最长相等前后缀

1、前缀:包含首字母但不包含尾字母的所有子串
eg:AABAAF:A、AA、AAB、AABA、AABAA
2、后缀:不包含首字母但包含尾字母的所有子串
eg:AABAAF:F、AF、AAF、BAAF、ABAAF
从上面的子串:AABAAF的前缀和后缀可以发现,前缀和后缀没有相同的,即最长相等前后缀为0。

三、前缀表next[]数组

1、前缀表
KMP算法的难点在于next数组的获取,而获取next数组的代码很简单但一点也不好理解,这里的不好理解在于为什么:j = next[j-1],而不是:j = j-1(刚开始看的时候百思不得其解,想哭)。这里我先把代码放上来叭。

public void getNext(int[] next, String s) {
        // 我这里j从0开始,很多人j从-1开始,这不冲突,因为我觉得j从0开始好理解一点
        int j = 0;
        next[0] = j;
        for (int i = 1; i < s.length(); i++) {
            while(j>0 && s.charAt(j) != s.charAt(i))
                j = next[j-1];
            if (s.charAt(j) == s.charAt(i))
                j++;
            next[i] = j;
        }
        // System.out.println(Arrays.toString(next));
}

下面表格为我通过一个个前后缀列举获得的next数组,方便后面和代码一起理解。
在这里插入图片描述

2、获取next[]数组的代码解释

int j = 0;
next[0] = j;

让next[0] = j:即next[0]=0,对应上表的单个字符,前后缀为空(通过前缀和后缀的定义可以理解)

for (int i = 1; i < s.length(); i++)

这里不难理解,因为j从0开始,所以i从1开始,这样才能保证字符子串的长度大于等于2。
代码中i和j的含义分别为:
① i:表示正在匹配的字符
② j:表示最长相等前后缀
(记住这句话,非常重要!)

while(j>0 && s.charAt(j) != s.charAt(i))
     j = next[j-1];
if (s.charAt(j) == s.charAt(i))
     j++;
next[i] = j;

这一块代码是KMP算法的核心,代码看似简单,但是是真的不好理解呀。下面我通过例子结合代码走一遍,希望能讲明白吧。
在这里插入图片描述

根据代码:

for (int i = 1; i < s.length(); i++) {
            while(j>0 && s.charAt(j) != s.charAt(i))
                j = next[j-1];
            if (s.charAt(j) == s.charAt(i))
                j++;
            next[i] = j;
        }
① j = 0; i = 1; s.charAt(0) != s.charAt(1),因为j不大于0,故退出while循环, next[1] = 0, i++
② j = 0; i = 2; s.charAt(0) != s.charAt(2),因为j不大于0,故退出while循环, next[2] = 0, i++
③ j = 0; i = 3; s.charAt(0) != s.charAt(3),因为j不大于0,故退出while循环, next[3] = 0, i++
④ j = 0; i = 4; s.charAt(0) == s.charAt(4),j++, next[i] = j ---> next[4] = 1, i++
⑤【回溯】j = 1; i = 5; s.charAt(1) != s.charAt(5), j = next[j-1]--->j = next[1-1] = next[0] = 0, 因为j不大于0,故退出while循环
  继续往下走:s.charAt(0) == s.charAt(5),j++, next[i] = j ---> next[5] = 1, i++
⑥ j = 1; i = 6; s.charAt(1) == s.charAt(6),j++, next[i] = j ---> next[6] = 2, i++
⑦ j = 2; i = 7; s.charAt(2) == s.charAt(7),j++, next[i] = j ---> next[7] = 3, i++
⑧ j = 3; i = 8; s.charAt(3) == s.charAt(8),j++, next[i] = j ---> next[6] = 4, i++  
⑨【回溯】j = 4; i = 9; s.charAt(4) != s.charAt(9), j = next[j-1]--->j = next[4-1] = next[3] = 0
⑩ j = 0; i = 10; s.charAt(0) == s.charAt(10),j++, next[i] = j ---> next[10] = 1, i++
退出for循环

下面解释为什么是:j = next[j-1]???而不是j = j-1
在这里插入图片描述
A、回到上述代码解释的第⑧步首先如果新加入的字符与前一个最长公共前缀子串的后一个字符相同:则j++;next[i] = j;即使:next[i] = j+1。
在这里插入图片描述
B、回到上述代码解释的第⑨步,s.charAt(4) != s.charAt(9)即B≠A,此时我们要回溯,回溯的过程中如下图所示:
在这里插入图片描述
这两个图是一样的,哪个看得明白看哪个。
在这里插入图片描述

根据上图,因为旧的最长相等前后缀(绿色部分)已经不适用了,我们要寻找新的最长相等前后缀(蓝色部分),根据前提条件:s[0, j-1] = s[i-j, i-1](这里注意刚开始:j=4,i=9,后续更新这里的j会变化但i不变),其实就是寻长度为j的子串的最长相等前后缀,而由前面计算的next数组可知,长度为j的最长相等前后缀为:next[j-1]。正是因此更新j:j = next[j-1]。假如一次回溯后:s.charAt(j) 依然不等于s.charAt(i),则重复上述回溯步骤j = next[j-1](依然注意j是变化的),直到s.charAt(j) == s.charAt(i)或者j=0但s.charAt(0) != s.charAt(i),则next[i]=0。(建议自己多举例子推一下就明白了,还有我这说的s[m,n],包括m也包括n)
1、最长相等前后缀为0:直到 j = 0,也不满足 s.charAt(j) == s.charAt(i) 且 s[0, j-1] = s[i-j, i-1]。
2、最长相等前后缀不为0:此时必满足 s.charAt(j) == s.charAt(i) 且 s[0, j-1] = s[i-j, i-1]

这里解释一下为什么j不可以是:j = j-1。通过上面的例子(为了方便看我把图片粘下来了)下图可以看到,当j=4,i=9时,s.charAt(0) != s.charAt(i),如果i不变,j通过j = j-1回溯,则当j=2的时候就会有s.charAt(2) == s.charAt(9) 但不满足:s[0, j-1] = s[i-j, i-1](s[0,1] != s[7, 8]),即使BA != AC。而通过j = next[j-1]这个更新条件可以确保满足s[0, j-1] = s[i-j, i-1]
在这里插入图片描述
最后说一句,写这篇文章的主要目的是让自己理清楚KMP算法,希望看到这篇文章对您也有所帮助。
参考:
暴力匹配部分引用了:数据结构 串 KMP 模式匹配详解 通俗易懂

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值