模式匹配算法(KMP算法详解)

1. 朴素的模式匹配算法

从主串S="goodgoogle"中找到T=“google’这个子串的位置

思路:
主串内设置指针i,子串内设置指针j,i与j同步开始向前进行比较,若出现不同,i回溯到上一次i开始位置的下一位,j回溯到0,继续比较

    public static int Index(String S,String T){
    
    
        int i =0;
        int j =0;
        char[] s =S.toCharArray();
        char[] t =T.toCharArray();
        while(i<s.length&&j<t.length){
    
             
            if(s[i]==t[j]){
    
    
                i++;
                j++;
            }
            else{
    
    
                i = i - j + 1;
                j = 0;
            }
        }
        if(j>=t.length){
    
    
            return i-j;
        }
        else{
    
    
            return 0;
        }

KMP算法

思路:
利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置

所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪?
在这里插入图片描述

如果最前面的k个字符[0~k]和j之前的最后k个字符[j-k~j-1]是一样的,那我们直接将j指针回溯到k的位置

为此,我们要找出指针j在最后一位时,如果失配,应该回溯到哪一位,也就是k指针的位置(k的值)

我们可以通过找出最长公共前后缀的方法来求k
如上图,ABCAB的最长公共前后缀为2,所以k=2(最前面的2个字符[0~2]和j之前的最后2个字符[j-2~j-1]是一样的,并且k指针指向第2位),当j在最后一位失配时,j应该回溯到子串第2位,也就是C的位置

最长前后缀的求法

参考阮一峰老师的文章

在这里插入图片描述

http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
作者: 阮一峰

失配查询表(Next 表)的求法

然而,最长前后缀表(pmt表)并不是最后的失配查询表(next表),因为当我们失配时,我们要查询的是失配位前一位的最长公共前后缀
因此,我们要将pmt表整体向后移动一格,再在第0位补上-1
在这里插入图片描述

求next数组代码实现

现在,我们再看一下如何编程快速求得next数组,这部分非常绕,我查看了许多博文,基本没有交代的很清楚的
先上代码

public static int[] getNext(String ps) {
    
    

    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    next[0] = -1;

    int j = 0;

    int k = -1;

    while (j < p.length - 1) {
    
    

       if (k == -1 || p[j] == p[k]) {
    
    
       	   ++j;
       	   ++k;
           next[j] = k;

       } else {
    
    

           k = next[k];

       }

    }

    return next;

}

假设子串比较指针为j,若失配要回溯到指针k的位置

先看特殊情况,子串第0位j=0时,就与父串指针i所指的位置不匹配,此时的做法与朴素算法无异,将子串整体向前移动一格,再次从子串第0位开始与父串比较。因此,next[0] = -1,指的是父串指针i要后移一位

j = 1时,next[1] = 0(必定是0,因为只有一个元素,没有最长公共前后缀)

下面是最重要的:
要求j+1指针指向位置的next值,就是看j位的最大公共前后缀的长度。

求j位的最大公共前后缀的长度,则首先需要看j-1位的next值。

(1) 若j-1位的next值为0(此时k必定等于0),那么j所在位置只需要与子串的第0位比较(因为前一位的最长公共前后缀为0,所以不可能拼凑出比1大的)

因此,此时比较p[j]与p[k]的值(p[k]是第0位):

  1. 若相等,则 j++(要赋值给j+1),k++(让k=1),next[j] = k(j+1位的next值为1)
  2. 若不相等,则k=next[k](让k=-1),进入下次循环,j++,k++,将j+1位的next值赋为0

(2) 若j-1位的next值不为0,为k(因为此时next[j] == k),则比较j指向的那一位与k指向的那一位是否相同:

  1. 若相等,则 j++(要赋值给j+1),k++(让k=k+1),next[j] = k(j+1位的next值为k+1)

  2. 若不等,则k回溯,k = next[k]

    难点!!!!!!!!!

为什么k要等于next[k]???

因为此时,k位置与j位置失配,已经不可能找到最长的最长公共前后缀了,但是仍然有希望去找到较小的最长公共前后缀。因此,我们要回去找除了k-1与j-1相配外,还有哪一位与j-1相配,这不正是next[j]所记录的嘛?

KMP算法实现

有了next数组后,KMP算法就相当简单

public static int KMP(String ts, String ps) {
    
    

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    int[] next = getNext(ps);

    while (i < t.length && j < p.length) {
    
    

       if (j == -1 || t[i] == p[j]) {
    
     // 当j为-1时,要移动的是i,当然j也要归0

           i++;

           j++;

       } else {
    
    

           // i不需要回溯了

           // i = i - j + 1;

           j = next[j]; // j回到指定位置

       }

    }

    if (j == p.length) {
    
    

       return i - j;

    } else {
    
    

       return -1;

    }

}

参考的一些博客:

https://www.cnblogs.com/yjiyjige/p/3263858.html
https://blog.csdn.net/qq_37969433/article/details/82947411
https://www.zhihu.com/question/21923021

猜你喜欢

转载自blog.csdn.net/m0_51082307/article/details/109590590