KMP算法

241 阅读6分钟

字符串匹配问题

给定一串字符串主串"ababaabcababc",再给定一串模式串"ababc",求模式串在主串中第一次出现的位置。通常我们最容易想到的求解方法是暴力解法。


字符串匹配算法(暴力解法)

将i作为主串S="ababaabcababc"的指针,从i=1开始。将j作为模式串T="ababc"的指针,从j=1开始。逐一比较主串和模式串的每一个字符,如果S[i]=T[j],则i和j同时加1,否则i回溯到i+1的位置,j回溯到1的位置,再重新依次比较。示意图如下所示。

首先比较第一个字符,

i对应的字符为a,j对应的字符也为a,所以i++,j++,依次类推,一直到i=5,j=5,如下图

                                                    

a和c不相等,i回溯到i=2的位置,j回溯到j=1的位置,继续依次比较,如下图

                                                    

核心算法如下所示:

public static int indexOf(String str,String pattern){
    char[] S = str.toCharArray();
    char[] T = pattern.toCharArray();
    for(int i=0;i<S.length;i++){
        int j=0;
        while (S[i]==T[j]){
            if(j==T.length-1) return j - T.length;
            i++;
            j++;
        }
    }
    return -1;
}

最坏情况下时间复杂度为O(Length(S)*Length(T))。


字符串匹配算法(KMP算法)

核心思想:主串S的i指针不回溯,模式串T的j指针回溯。引入next[j]数组,数组的长度等于模式串T的长度,数组位置j的值表示的含义是当模式串在位置j和主串S不匹配时,模式串需要回溯到的位置。我们依然用上面的主串S和模式串T为例来演示。

依次比较各个字符,直到i=5,j=5,如下图

                                                     

此时,a和c不等。我们将图示模式串T向右移动一位,主串和指针i,j不变,如下图

                                                     

我们发现主串S的第二个位置元素b和模式串T的第一个位置a不相等,所以这是一次无用的比较,如果我们将指针i回溯到字符b的位置,相当于是做了一次无用功,我们希望回避这种无用的指针回溯。我们再将模式串T向右移动一位,主串和指针i,j不变,如下图

                                                     

此时,主串S的第三个字符a与第四个字符b和模式串T的第一个字符a和第二个字符b是相等的。我们就可以再比较主串S的i位置的字符和模式串T的j位置的字符是否相等。我们可以发现,整个过程i的指针位置是没有回溯的,而移动模式串T的位置,相当于回溯了j的指针。而且可以发现,模式串T的元素c之前的子串为“abab”,其最大公共前后缀长度为2,最大公共前后缀为ab,所以next[j]的值为2+1=3。继续往下比较,S[i]=a,T[j]=a,S[i]=T[j],则i++,j++,如下图                                             

                                                      

此时,S[i]=a,T[j]=b,S[i] !=T[j],所以我们找到模式串T的元素b之前的子串为"aba",其最大公共前后缀长度为1,最大公共前后缀为a,所以next[j]的值为1+1=2。j指针回溯到2的位置继续比较,如下图

                                                     

此时,S[i]=a,T[j]=b,S[i]!=T[j],由于此时j已经在位置2上了,只会确定回溯到位置1处。如下图

                                                     

继续比较,发现后面两个字符都是相等的。一直到j移到位置3的时候,如下图

                                                     

此时,S[i]=c,T[j]=a,S[i]!=T[j],而T[j]钱面的子串"ab"的最长公共前后缀长度为0,所以j回溯到0+1=1的位置,如下图

                                                    

此时,S[i]=c,T[j]=a,S[i]!=T[j],而此时j又已经在位置1的位置,没有回溯的空间了,所以i和j同时加1,再进行比较,就找到了匹配的子串。我们用next[j]表示模式串T中位置j处的元素的下一个回溯位置。通过上面的分析,我们可以得出KMP算法的一个大致的算法思路如下。

public static int indexOf(String str,String pattern){
    char[] S = str.toCharArray();
    char[] T = pattern.toCharArray();
    int[] next = new int[T.length];
    getNext(pattern,next);
    int i = 0;
    int j = 0;
    while(i < S.length && j<T.length){
        if(j==0 || S[i]==T[j]) {
            i++;
            j++;
            if(j >= T.length) return i - T.length;
        }
        else j = next[j];
    }
    return -1;
}

可以看到,在不考虑求next数组的时间复杂度的情况下,KMP算法最坏情况下的时间复杂度为O(Length(S))。下面,我们就将关注点放在next数组怎么求上面。我们直接给出结论:初始条件为next[1]=0。当next[j] = k,并且T[j] = T[k]时,则next[j+1] = k+1。当T[j] != T[k]时,此时即使j回溯到了k的位置,字符串还是不匹配的,所以j需要继续回溯。因此我们给出求next数组的算法。

public static void getNext(String pattern,int[] next){
    char[] T = pattern.toCharArray();
    int i = 0,j = -1;
    next[0] = -1;
    while (i < T.length-1){
        if(j==-1 || T[i]==T[j]){
            ++i;
            ++j;
            next[i] = j;
        }else{
            j = next[j];
        }
    }
}

可以看到,求next数组的算法时间复杂度为O(Length(T))。所以我们整个KMP算法的时间复杂度为O(Length(S)+Length(T))。

参考资料:

1、https://www.cnblogs.com/ZuoAndFutureGirl/p/9028287.html

2、严蔚敏(数据结构视频课程)-12集