KMP算法过程


题目:给定两个字符串s1s2,判断s2是否是s1的子串,如果是则返回s2首次出现在s1的下标位置。

s1=AAAAAAAB, s2=AAAAB


暴力算法

思路

暴力算法思路如下

  1. 使用index1表示s1的字符下标,index2表示s2的字符下标
  2. s1的第i(i从0开始)个位置和s2的第0个位置开始匹配,此时index1 = iindex2 = 0
  3. 遇到字符相等,则向前推进,即index1++index2++
  4. 遇到字符不相等,则退出匹配过程,进入下一轮匹配。
    下一轮开始时index1 = i + 1index = 0,相当于从s1的第(i+1)个位置重新开始匹配

匹配过程

匹配过程如下所示

第一轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
匹配结果 ×

第一轮比较开始,从s1的第1个位置与s2的第一个位置开始比较,index1 = 0index2 = 0

如果s1[index1] = s2[index2],则index1++index2++,继续往下比较

index1 = 4index2 = 4

s1[index1] != s2[index2],退出比较,进入下一轮比较。


第二轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
匹配结果 ×

第二轮比较开始,从s1的第2个位置与s2的第一个位置开始比较,index1 = 1index2 = 0

如果s1[index1] = s2[index2],则index1++index2++,继续往下比较

index1 = 5index2 = 4

s1[index1] != s2[index2],退出比较,进入下一轮比较。


第三轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
匹配结果 ×

第三轮比较开始,从s1的第3个位置与s2的第一个位置开始比较,index1 = 2index2 = 0

如果s1[index1] = s2[index2],则index1++index2++,继续往下比较

index1 = 6index2 = 4

s1[index1] != s2[index2],退出比较,进入下一轮比较。


第四轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
匹配结果

第四轮比较开始,从s1的第4个位置与s2的第一个位置开始比较,index1 = 3index2 = 0

如果s1[index1] = s2[index2],则index1++index2++,继续往下比较

index1 = 8index2 = 5时,退出匹配过程

因为index2 = s2.lenght,匹配成功,返回3


假设s1的长度为Ns2的长度为M,最坏时间复杂度是O(NM)

代码如下

/**
     * 暴力求解
     * 判断s2是否是s1的子串, 返回s2首次出现在s1的下标
     * 匹配思路
     * 1. 使用index1表示s1的字符下标, index2表示s2的字符下标
     * 2. 从s1的第i(i从0开始)个位置和s2的第0个位置开始匹配, 此时index1 = i, index2 = 0
     *    如果遇到不相等的字符, 则退出匹配过程
     *    则令index1 = i + 1, index = 0, 进入下一轮匹配, 相当于从s1的第(i+1)个位置重新开始匹配
     */
    public static int blIndexOf(String s1, String s2) {
        if (s1 == null || s2 == null || s1.length() < s2.length()) {
            return -1;
        }
        char[] str1 = s1.toCharArray();
        char[] str2 = s2.toCharArray();
        for (int i = 0; i < str1.length; i++) {
            // 从s1的第i个位置开始与s2的第0个位置开始匹配
            int index1 = i, index2 = 0;
            while (index1 < s1.length() && index2 < s2.length()) {
                if (str1[index1] == str2[index2]) {
                    // 如果字符相等, 则两个下标向前推进, 判断下一个字符是否还相等
                    index1++;
                    index2++;
                } else {
                    // 遇到不同的字符, 说明从s1的第i个位置开始, 无法匹配到s2, 退出匹配过程
                    break;
                }
            }
            // 如果index2等于s2长度, 说明匹配成功
            if (index2 == s2.length()) {
                // return index1 - index2;
                return i;
            }
        }
        return -1;
    }

KMP算法

KMP算法中需要用到前缀子串和后缀子串的概念。

比如字符串ABBABBC,计算字符C的前缀子串,字符C前面的字符串为ABBABB

当长度为1时:前缀=A,后缀=B,前缀不等于后缀

当长度为2时:前缀=AB,后缀=BB,前缀不等于后缀

当长度为3时:前缀=ABB,后缀=ABB,前缀等于后缀

当长度为4时:前缀=ABBA,后缀=BABB,前缀不等于后缀

当长度为5时:前缀=ABBAB,后缀=BBABB,前缀不等于后缀

当长度为6时:前缀跟后缀不能等于整体

因此,字符C[最长前缀跟最长后缀相等的长度] 等于3

匹配过程

在KMP算法中,会使用到**[最长前缀与最长后缀相等的长度]**,对于字符串ABBABBC,字符C最长前缀与最长后缀相等时的长度为3

KMP整体思路跟暴力算法是一样的,只是在匹配失败后不会每次都从头开始比较,会有一个加速过程。


KMP具体流程如下

使用index1表示s1的字符下标,index2表示s2的字符下标


第一轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
开始比较
匹配结果 ×

第一轮比较,从s1的第1个字符(index1=0)和s2的第1个字符(index2=0)开始逐个比较。

index1=4、index2=4时匹配失败(s1[4]!=s2[4])。

s1s2的前四个字符AAAA相等。字符s2[4][最长前缀等于最长后缀的长度] 为3。

可以得出:s1[4]前面的三个字符肯定等于s2的前三个字符,所以可以直接将s2的前三个字符跟s1[4]前面的三个字符对其,然后进入下一轮比较。

进入下一轮时,index1=4不变,index2=3index2不需要从头开始,这里可以减少3次不必要的比较。


第二轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
开始比较
匹配结果 ×

第二轮比较,从s1的第5个字符(index1=4)和s2的第4个字符(index2=3)开始逐个比较。

index1=5、index2=4时匹配失败(s1[5]!=s2[4])。

因为字符s2[4][最长前缀等于最长后缀的长度] 为3,因此s1[5]前面的三个字符肯定等于s2的前三个字符,直接将s2的前三个字符跟s1[5]前面的三个字符对其,然后进入下一轮比较。

进入下一轮时,index1=5不变,index2=3index2不需要从头开始,这里可以减少3次不必要的比较。


第三轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
开始比较
匹配结果 ×

第三轮比较,从s1的第6个字符(index1=5)和s2的第4个字符(index2=3)开始逐个比较。

index1=6、index2=4时匹配失败(s1[6]!=s2[4])。

因为字符s2[4][最长前缀等于最长后缀的长度] 为3,因此s1[5]前面的三个字符肯定等于s2的前三个字符,直接将s2的前三个字符跟s1[5]前面的三个字符对其,然后进入下一轮比较。

进入下一轮时,index1=6不变,index2=3index2不需要从头开始,这里可以减少3次不必要的比较。


第四轮比较

s1下标 0 1 2 3 4 5 6 7
s1 A A A A A A A B
s2 A A A A B
s2下标 0 1 2 3 4
开始比较
匹配结果

第四轮比较,从s1的第7个字符(index1=6)和s2的第4个字符(index2=3)开始逐个比较。

最后能走到s1[7]=s2[4],匹配完成。


可以发现,KMP算法遇到不同字符进入下一轮比较时,如果s2[index2](此时的index2为匹配失败的位置)的 [最长前缀等于最长后缀的长度] 大于0,那么进入下一轮比较时index1不会变,index2也不会从头开始,这样就可以减少比较次数,达到了加速的效果。

为了达到加速效果,我们需要知道字符串s2每个字符的 [最长前缀等于最长后缀的长度] ,KMP算法在匹配前需要计算一个next数组,这个next数组保存了s2每个字符的 [最长前缀等于最长后缀的长度]

求解next数组

next求解过程,假设字符串s的长度为ni的范围为[0,n-1]snext数组长度为n

人为规定next[0]=-1,因为s[0]前面没有任何字符,也就相当于没有前缀

人为规定next[1]=0,因为next[0]前面只有一个字符,但是前缀跟后缀不能等于整体,所以next[1]=0


下面开始讨论i>1 && i < n的情况

使用cn保存 [最长前缀等于最长后缀的长度]cn也是与s[i-1]对比的下标。相当于s的前cn个字符与s[i-1]的前cn个字符相等。理解cn含义对于理解整个求解next数组过程很重要

next[0]=-1;next[1]=0;

i = 2;

cn = next[i - 1];

④开始计算next[i]

  1. 如果s[i-1] = s[cn],则

    next[i] = cn + 1;
    i++;
    cn++;
    

    继续执行步骤④。

  2. 如果s[i-1] != s[cn] && cn > 0,则cn=next[cn]cn往前推进,然后继续执行步骤④,该步骤不太容易理解

  3. 如果s[i-1] != s[cn] && cn <= 0,说明此时s[i]不存在相等的前缀与后缀了,cn不能往前走了

    next[i] = 0;
    i++;
    

求解next数组过程,当s[i-1] != s[cn] && cn > 0cn=next[cn],这个步骤不太容易理解,需要通过例子模拟这个过程才容易理解。

为了理解步骤④,举个例子,假设s=ABBSTABBECABBSTABB?C,下标为[0,18]next数据已经求出来了,下面开始求next[19]


假设s[18]=E

s A B B S T A B B E C A B B S T A B B E C
下标 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
next -1 0 0 0 0 0 1 2 3 0 0 1 2 3 4 5 6 7 8

i=19时,cn=8,说明s的前8个字符与s[18]的前8个字符是相等的

此时s[i-1] = s[cn],即s[18] = s[8],说明s的前9个字符与s[19]的前9个字符相等,因此next[19] = cn + 1 = 9

然后i++i=20,令cn++,继续往下。


假设s[18]=S

s A B B S T A B B E C A B B S T A B B S C
下标 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
next -1 0 0 0 0 0 1 2 3 0 0 1 2 3 4 5 6 7 8

i=19时,cn=next[i-1]=next[18]=8,说明s的前8个字符与s[18]的前8个字符是相等的。

①此时s[i-1] != s[cn] && cn > 0,即s[18] != s[8] && 8 > 0,说明s的前9个字符与s[19]的前9个字符不相等,但是s的前8个字符与s[18]的前8个字符是相等的,所以让cn往前推进

这个时候cn = next[cn] = next[8] = 3,说明s的前3个字符与s[18]的前3个字符是相等的。

②此时s[i-1] = s[cn],即s[18] = s[3],说明s的前4个字符与s[19]的前4个字符相等,因此next[19] = cn + 1 = 9

然后i++i=20,令cn++,继续往下。


假设s[18]=T

s A B B S T A B B E C A B B S T A B B T C
下标 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
next -1 0 0 0 0 0 1 2 3 0 0 1 2 3 4 5 6 7 8

i=19时,cn=next[i-1]=next[18]=8,说明s的前8个字符与s[18]的前8个字符是相等的。

①此时s[i-1] != s[cn] && cn > 0,即s[18] != s[8] && 8 > 0,说明s的前9个字符与s[19]的前9个字符不相等,但是s的前8个字符与s[18]的前8个字符是相等的,所以让cn往前推进

这个时候cn = next[cn] = next[8] = 3,说明s的前3个字符与s[18]的前3个字符是相等的。

②此时s[i-1] != s[cn] && cn > 0,即s[18] != s[3]&& 3 > 0,说明s的前4个字符与s[19]的前4个字符不相等,但是s的前3个字符与s[18]的前3个字符是相等的,所以让cn往前推进

这个时候cn = next[cn] = next[3] = 0,说明s的前0个字符与s[i-1]的前0个字符相等。

③此时s[i-1] != s[cn] && cn <= 0,说明找不到s[19]前面的n(n>0)个字符等于s的前n(n>0)个字符,即 [最长前缀等于最长后缀的长度] 为0,然后执行

next[19] = 0;
i++;

Java代码

public class KMP {
    /**
     * 判断 m 是否是 s 的子串并返回 m 首次出现在 s 的下标
     * 比如 s = abcdefg
     * 若 m = def, 则返回3; 若 m = bbb, 则返回 -1
     * 这里需要知道一个概念:前缀子串跟后缀子串
     * 比如字符串:abbabb_ , 计算下划线前的前缀子串与后缀子串
     * 长度为1时:前缀 = a, 后缀 = b, 前缀 != 后缀
     * 长度为2时:前缀 = ab, 后缀 = bb, 前缀 != 后缀
     * 长度为3时:前缀 = abb, 后缀 = abb, 前缀 = 后缀
     * 长度为4时:前缀 = abba, 后缀 = babb, 前缀 != 后缀
     * 长度为5时:前缀 = abbab, 后缀 = bbabb, 前缀 != 后缀
     * 长度为6时:前缀跟后缀不能等于整体
     *
     * KMP算法中会用到一个辅助数组next,
     * 这个数组记录的就是子串字符串在下标为i位置时最长前缀与最长后缀相等时的前缀长度(或者后缀长度)
     * 比如字符串 m = abbabbc
     * 根据上面的例子可以知道, i = 6 时, 最长前缀等于最长后缀的长度为3, 因此next[6]=3
     * next[0] = -1, 因为下标为0之前已经没有字符了, 所以规定next[0] = -1
     * next[1] = 0, 因为下标为1之前只有一个字符,因为前缀跟后缀不能等于整体,所以next[1]=0
     *
     * 有了这个辅助数组,在进行字符匹配的时候就可以减少很多次重复匹配
     * 比如 s = abbabbabbs, m = abbabbs
     * 第一次: i1 = 6, i2 = 6, s[6] = a, m[6] = s, s[6] != m[6]
     *        此时出现第一次不相等, 通过m的next数组可以知道
     *        对于m, 下标为6时, 它的最长前缀等于最长后缀的长度为3, s[6] != m[6]时, 令 i2 = next[i2] = 3
     *        此时 i1 = 6, i2 = 3
     */
    public static int getIndexOf(String s, String m) {
        if (s == null || m == null || s.length() < m.length()) {
            return -1;
        }
        char[] str1 = s.toCharArray();
        char[] str2 = m.toCharArray();
        // 获取next数组
        int[] next = getNextArray(str2);
        // i1 记录 str1 的下标, i2 记录 str2 的下标
        int i1 = 0, i2 = 0;
        while (i1 < str1.length && i2 < str2.length) {
            if (str1[i1] == str2[i2]) {
                // 当前字符相等
                i1++;
                i2++;
            } else if (next[i2] == -1) {
                // next[i2] == -1, 说明已经没有前缀了(str2[0]匹配失败), 只能让 str1 的下标往前走
                i1++;
            } else {
                // i2 往前走
                i2 = next[i2];
            }
        }
        return i2 == str2.length ? i1 - i2 : -1;
    }

    /**
     * next数组的计算逻辑
     * 人为规定 next[0] = -1;
     */
    private static int[] getNextArray(char[] str) {
        if (str.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[str.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;
        /**
         * cn 代表了两个含义
         * 1. 最长前缀等于最长后缀的长度
         * 2. 与 i-1位置进行比较的下标
         *    如果 str[cn] == str[i-1], 则next[i] = next[i-1]+1
         *    如果 str[cn] != str[i-1] && cn > 0, 则 cn = next[cn]
         *    如果 str[cn] != str[i-1] && cn <= 0, 则next[i] = 0
         */
        int cn = next[i - 1];
        while (i < str.length) {
            if(str[cn] == str[i - 1]) {
                next[i] = cn + 1;
                i++;
                /**
                 * 因为i已经加1了, 所以cn记录的应该是i加1之前的长度, 因此cn也要加1
                 * 相当于 cn = next[i-1]
                 */
                cn++;
            } else if (cn > 0) {
                cn = next[cn];
            } else {
                // cn 已经不能往前走了
                next[i] = 0;
                i++;
            }
        }
        return next;
    }

    public static void main(String[] args) {
        String str = "ABBSTABBECBBSTABBEC111111";
        String match = "ABBSTABBECABBSTABBSC";
        System.out.println(getIndexOf(str, match));
        System.out.println(str.indexOf(match));
    }
}

你可能感兴趣的:(算法学习,算法,数据结构)