KMP算法——通俗易懂讲好KMP算法:实例图解分析+详细代码注解 --》你的所有疑惑在本文都能得到解答

news2025/8/4 1:28:55

1.kmp算法基本介绍

  • KMP 是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法。
  • Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP 算法”,常用于在一个文本串 S 内查找一个模式串 P 的出现位置,这个算法由Donald KnuthVaughan PrattJames H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。
  • KMP 方法算法就利用之前判断过的信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间。

2.字符串的最长公共前后缀&部分匹配表

2.1 什么是最长公共前后缀

1️⃣ 字符串的前缀是指不包含最后一个字符所有以第一个字符(索引为0)开头的连续子串

比如字符串 “ABABA” 的前缀有:A,AB,ABA,ABAB

2️⃣ 字符串的后缀是指不包含第一个字符所有以最后一个字符结尾的连续子串

比如字符串 “ABABA” 的后缀有:BABA,ABA,BA,A

3️⃣ 公共前后缀:一个字符串的 所有前缀连续子串 和 所有后缀连续子串 中相等的子串

比如字符串 “ABABA”

  • 前缀有:A,AB,ABA,ABAB
  • 后缀有:BABA,ABA,BA,A

因此公共前后缀有:AABA

4️⃣ 最长公共前后缀:所有公共前后缀 的 长度最长的 那个子串

比如字符串 “ABABA” ,公共前后缀有:AABA

由于 ABA 是 三个字符长度,A 是一个字符长度,那么最长公共前后缀就是 ABA

📝 再比如说一个字符串 str = “ABCABD”

  • 对于str从 索引为0 开始的子串 “A” 而言:

    1. 前缀:不包含最后一个字符A的 所有以第一个字符A开头 的 连续子串 不存在
    2. 后缀:不包含第一个字符A 的 所有以最后一个字符A结尾 的 连续子串 不存在

    因此该子串的最长公共前后缀 为 0

  • 对于str从 索引为0 开始的子串 “AB” 而言:

    1. 前缀:不包含 最后一个字符B 的 所有以第一个字符A开头 的 连续子串 有 —— “A
    2. 后缀:不包含 第一个字符A 的 所有以最后一个字符B结尾 的 连续子串 有 —— “B

    因此该子串的最长公共前后缀 为 0

  • 对于str从 索引为0 开始的子串 “ABC” 而言:

    1. 前缀:不包含 最后一个字符C 的 所有以第一个字符A开头 的 连续子串 有 —— “A”,“AB
    2. 后缀:不包含 第一个字符A 的 所有以最后一个字符C 结尾 的 连续子串有 —— “BC”,“C

    前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0

  • 对于str从 索引为0 开始的子串 “ABCA” 而言:

    1. 前缀:不包含 最后一个字符A 的 所有以第一个字符A开头 的 连续子串 有 —— “A”,“AB”,“ABC
    2. 后缀:不包含 第一个字符A 的 所有以最后一个字符A 结尾 的 连续子串有 —— “BCA”,“CA”,“A

    前缀与后缀的连续子串中存在相同且最长的子串 A因此该子串的最长公共前后缀 为 1

  • 对于str从 索引为0 开始的子串 “ABCAB” 而言:

    1. 前缀:不包含 最后一个字符B 的 所有以第一个字符A开头 的 连续子串 有 —— “A”,“AB”,“ABC”,“ABCA
    2. 后缀:不包含 第一个字符A 的 所有以最后一个字符B 结尾 的 连续子串有 —— “BCAB”,“CAB”,“AB”,“B

    前缀与后缀的连续子串中存在相同且最长的子串 AB因此该子串的最长公共前后缀 为 2

  • 对于str从 索引为0 开始的子串 “ABCABD” 而言:

    1. 前缀:不包含 最后一个字符D 的 所有以第一个字符A开头 的 连续子串 有 —— “A”,“AB”,“ABC”,“ABCA”,“ABCAB
    2. 后缀:不包含 第一个字符A 的 所有以最后一个字符D 结尾 的 连续子串有 —— “BCABD”,“CABD”,“ABD”,“BD”,“D

    前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0

2.2 什么是部分匹配表Next

个人理解:对于字符串str,从 第一个字符开始的每个子串最后一个字符该子串的最长公共前后缀的长度 的对应关系表格。这个表我们以 int[] next 数组方式进行存储。

比如说上面举的例子:

  • 子串 “A”:最后一个字符是 A,该子串的最长公共前后缀长度是 0,因此对应关系就是 A - 0
  • 子串 “AB”:最后一个字符是 B,该子串的最长公共前后缀长度是 0,因此对应关系就是 B - 0
  • 子串 “ABC”:最后一个字符是 C,该子串的最长公共前后缀长度是 0,因此对应关系就是 C - 0
  • 子串 “ABCA”:最后一个字符是 A,该子串的最长公共前后缀长度是 1,因此对应关系就是 A - 1
  • 子串 “ABCAB”:最后一个字符是 B,该子串的最长公共前后缀长度是 2,因此对应关系就是 B - 2
  • 子串 “ABCABD”:最后一个字符是 D,该子串的最长公共前后缀长度是 0,因此对应关系就是 D - 0

因此综上,我们说该字符串 str 的部分匹配表为:

ABCABD
000120

那么对应的next数组就是 int[] next = {0, 0, 0, 1, 2, 0}

2.3 字符串最长公共前后缀&部分匹配表的代码实现

image-20221123152149707

下面的代码可以参考图中实例进行分析:

    /**
     * 获取一个字符串 pattern 的部分匹配表
     *
     * @param patternStr 用于模式匹配字符串
     * @return 存储部分匹配表的每个子串的最长公共前后缀的 next数组
     */
    public static int[] kmpNext(String patternStr) {
        //将 patternStr 转为 字符数组形式
        char[] patternArr = patternStr.toCharArray();

        //预先创建一个next数组,用于存储部分匹配表的每个子串的最长公共前后缀
        int[] next = new int[patternStr.length()];

        /*
            从第一个字符(对应索引为0)开始的子串,如果子串的长度为1,那么肯定最长公共前后缀为0
            因为这唯一的一个字符既是第一个字符,又是最后一个字符,所以前后缀都不存在 -> 最长公共前后缀为0
         */
        next[0] = 0;

        /*
          len有两个作用:
            1. 用于记录当前子串的最长公共前后缀长度
            2. 同时知道当前子串的最长公共前后缀的前缀字符串对应索引 [0,len-1]/当前子串最长公共前缀字符串的下一个字符的索引  <-- 可以拿示例分析一下
         */
        int len = 0;

        //从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度
        int i = 1;
        while (i < patternArr.length) {
            /*
                1.已经知道了上一个子串 对应索引[0,i-1] 的最长公共前后缀长度为 len
                  的前缀字符串是 索引[0,len-1],对应相等的后缀字符串是 索引[i-len,i-1]

                2.因此我们可以以 上一个子串的最长公共前后缀字符串 作为拼接参考
                  比较一下 patternArr[len] 与 patternArr[i] 是否相等
             */
            if (patternArr[len] == patternArr[i]) {
                /*
                    1.如果相等即 patternArr[len]==patternArr[i],
                      那么就可以确定当前子串的最长公共前后缀的
                      前缀字符串是 索引[0,len] ,对应相等的后缀字符串是 索引[i-len,i]

                    2.由于是拼接操作,那么当前子串的最长公共前后缀长度只需要在上一个子串的最长公共前后缀长度的基础上 +1 即可
                      即 next[i] = next[i-1] + 1 ,

                    3.由于 len 是记录的子串的最长公共前后缀长度,对于当前我们所在的代码位置而言
                      len 还是记录的上一个子串的最长公共前后缀长度,因此:
                      next[i] = next[i-1] + 1 等价于 next[i] = ++len
                 */

                // 等价于 next[i] = next[i-1] + 1
                next[i] = ++len;
                //既然找到了索引在[0,i]的子串的最长公共前后缀字符串长度,那就 i+1 去判断以下一个字符结尾的子串的最长公共前后缀长度
                i++;
            }else {
                /*
                    1.如果不相等 patternArr[len]!=patternArr[i]
                      我们想要求当前子串 对应索引[0,i] 的最长公共前后缀长度
                      我们就不能以 上一个子串的最长公共前后缀:前缀字符串pre  后缀字符串post (毫无疑问pre==post) 作为拼接参考

                    2.但可以思考一下:
                      pre的最长公共前缀字符串: 索引 [      0        , next[len-1] )
                      是等于
                      post的最长公共后缀字符串:索引 [ i-next[len-1] ,     i       )

                      则我们 就以 pre的最长公共前缀字符串/post的最长公共后缀字符串 作为拼接参考
                      去判断 pre的最长公共前缀字符串的下一个字符patternArr[next[len-1]] 是否等于 post的最长公共后缀字符串的下一个字符patternArr[i]

                    3.在第 1,2 步分析的基础上
                      我们可以在判断出 patternArr[len]!=patternArr[i] 后,
                      不去执行第二步:patternArr[next[len-1]] 是否等于 patternArr[i],
                      可以先修改len的值:len = next[len-1],len就成了 pre的最长公共前缀字符串长度/post的最长公共后缀字符串长度,
                      修改完之后,再去判断下一个字符 是否相等,即 判断 patternArr[len] 是否等于 patternArr[i]
                      仔细观察,这不又是在判断 这个循环中 if-else 语句吗

                    4.关于 len 这个值,在循环开始时我们解释的是:上一个子串的最长公共前后缀字符串的长度
                      但实际上我们在这里改为 len = next[len-1] 表示上一个子串的最长公共前后缀字符串的最长公共前后缀字符串的长度
                      是没有问题的,等价于上一个子串的较小的公共前后缀字符串。
                      既然进入了 else 语句说明字符不相等,就不能以 上一个子串的最长公共前后缀字符串 作为 拼接参考,就应当去缩小参考范围。
                 */
                if(len==0) {

                    /*
                        len为0说明上一个子串已经没有了公共前后缀字符串
                        则我们没有继续寻找的必要 --> 索引在[0, i]的当前子串的最长公共前后缀字符串长度就是0
                     */
                    next[i] = len;

                    //继续寻找下一个字符串的最长公共前后缀字符串长度
                    i++;

                }else{
                    len = next[len - 1];
                }

            }
        }

        return next;
    }

2.4 代码测试

package kmp;

import java.util.Arrays;

/**
 * @author 狐狸半面添
 * @create 2022-11-22 22:43
 */
public class KMPAlgorithm {
    public static void main(String[] args) {
        String patternStr = "ABCDABD";
		//输出结果:[0, 0, 0, 0, 1, 2, 0]
        System.out.println(Arrays.toString(kmpNext(str2)));
    }

    /**
     * 获取一个字符串 pattern 的部分匹配表
     *
     * @param patternStr 用于模式匹配字符串
     * @return 存储部分匹配表的每个子串的最长公共前后缀的 next数组
     */
    public static int[] kmpNext(String patternStr) {
        //将 patternStr 转为 字符数组形式
        char[] patternArr = patternStr.toCharArray();

        //预先创建一个next数组,用于存储部分匹配表的每个子串的最长公共前后缀
        int[] next = new int[patternStr.length()];

        /*
            从第一个字符(对应索引为0)开始的子串,如果子串的长度为1,那么肯定最长公共前后缀为0
            因为这唯一的一个字符既是第一个字符,又是最后一个字符,所以前后缀都不存在 -> 最长公共前后缀为0
         */
        next[0] = 0;

        /*
          len有两个作用:
            1. 用于记录当前子串的最长公共前后缀长度
            2. 同时知道当前子串的最长公共前后缀的前缀字符串对应索引 [0,len-1]/当前子串最长公共前缀字符串的下一个字符的索引  <-- 可以拿示例分析一下
         */
        int len = 0;

        //从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度
        int i = 1;
        while (i < patternArr.length) {
            /*
                1.已经知道了上一个子串 对应索引[0,i-1] 的最长公共前后缀长度为 len
                  的前缀字符串是 索引[0,len-1],对应相等的后缀字符串是 索引[i-len,i-1]

                2.因此我们可以以 上一个子串的最长公共前后缀字符串 作为拼接参考
                  比较一下 patternArr[len] 与 patternArr[i] 是否相等
             */
            if (patternArr[len] == patternArr[i]) {
                /*
                    1.如果相等即 patternArr[len]==patternArr[i],
                      那么就可以确定当前子串的最长公共前后缀的
                      前缀字符串是 索引[0,len] ,对应相等的后缀字符串是 索引[i-len,i]

                    2.由于是拼接操作,那么当前子串的最长公共前后缀长度只需要在上一个子串的最长公共前后缀长度的基础上 +1 即可
                      即 next[i] = next[i-1] + 1 ,

                    3.由于 len 是记录的子串的最长公共前后缀长度,对于当前我们所在的代码位置而言
                      len 还是记录的上一个子串的最长公共前后缀长度,因此:
                      next[i] = next[i-1] + 1 等价于 next[i] = ++len
                 */

                // 等价于 next[i] = next[i-1] + 1
                next[i] = ++len;
                //既然找到了索引在[0,i]的子串的最长公共前后缀字符串长度,那就 i+1 去判断以下一个字符结尾的子串的最长公共前后缀长度
                i++;
            }else {
                /*
                    1.如果不相等 patternArr[len]!=patternArr[i]
                      我们想要求当前子串 对应索引[0,i] 的最长公共前后缀长度
                      我们就不能以 上一个子串的最长公共前后缀:前缀字符串pre  后缀字符串post (毫无疑问pre==post) 作为拼接参考

                    2.但可以思考一下:
                      pre的最长公共前缀字符串: 索引 [      0        , next[len-1] )
                      是等于
                      post的最长公共后缀字符串:索引 [ i-next[len-1] ,     i       )

                      则我们 就以 pre的最长公共前缀字符串/post的最长公共后缀字符串 作为拼接参考
                      去判断 pre的最长公共前缀字符串的下一个字符patternArr[next[len-1]] 是否等于 post的最长公共后缀字符串的下一个字符patternArr[i]

                    3.在第 1,2 步分析的基础上
                      我们可以在判断出 patternArr[len]!=patternArr[i] 后,
                      不去执行第二步:patternArr[next[len-1]] 是否等于 patternArr[i],
                      可以先修改len的值:len = next[len-1],len就成了 pre的最长公共前缀字符串长度/post的最长公共后缀字符串长度,
                      修改完之后,再去判断下一个字符 是否相等,即 判断 patternArr[len] 是否等于 patternArr[i]
                      仔细观察,这不又是在判断 这个循环中 if-else 语句吗

                    4.关于 len 这个值,在循环开始时我们解释的是:上一个子串的最长公共前后缀字符串的长度
                      但实际上我们在这里改为 len = next[len-1] 表示上一个子串的最长公共前后缀字符串的最长公共前后缀字符串的长度
                      是没有问题的,等价于上一个子串的较小的公共前后缀字符串。
                      既然进入了 else 语句说明字符不相等,就不能以 上一个子串的最长公共前后缀字符串 作为 拼接参考,就应当去缩小参考范围。
                 */
                if(len==0) {

                    /*
                        len为0说明上一个子串已经没有了公共前后缀字符串
                        则我们没有继续寻找的必要 --> 索引在[0, i]的当前子串的最长公共前后缀字符串长度就是0
                     */
                    next[i] = len;

                    //继续寻找下一个字符串的最长公共前后缀字符串长度
                    i++;

                }else{
                    len = next[len - 1];
                }

            }
        }

        return next;
    }
}

3.根据部分匹配表搜索字符串匹配位置

3.1 匹配成功一个就退出匹配的代码

3.1.1 KMP算法的大致步骤

  1. 求出模式字符串patternStr的部分匹配表,已知待匹配的字符串 matchStr

  2. 定义两个指针 ij,分别指向 patternStr 和 matchStr ,初始化为0

  3. 判断 patternStr[i] 和 matchStr[j] 是否相等

    • 如果相等,则继续向后匹配:i++, j++

    • 如果不相等,则 i 不变,调整 j 为 模式字符串pattern 上一个子串(索引 [ 0, j-1 ])的最长公共前缀字符串的下一个索引位置,该索引位置也是最长公共前缀/后缀字符串的长度:j = next[ j - 1 ]

      解释一下不相等为什么要这样做:

      1️⃣ 在不相等的时候,我们可以知道前面已经匹配的字符串 str1str2 肯定是完全相等的

      • str1:在 matchStr 中对应索引 [ i - j , i - 1 ]
      • str2:在 patternStr 中对应索引 [ 0 , j - 1 ]

      2️⃣ 由于 完全相等,则 str1 的 最长公共后缀字符串 一定等于 str2 的 最长公共前缀字符串,那么:

      • 将 i 定位到 str1 的 最长公共后缀字符串 的 下一个字符位置,但很明显,i 此时的位置肯定已经是在 str1 的最长公共后缀字符串的下一个字符的位置,因此 i 的值不需要做调整
      • 将 j 定位到 str2 的 最长公共前缀字符串 的 下一个字符位置next [ j - 1] 不仅代表 上一个子串str1的最长公共前后缀字符串长度,也是最长公共前缀字符串的下一个字符的索引。因此,我们只需要:j = next[ j - 1 ]

      3️⃣ 修改完毕,我们此时已经匹配的字符串:

      • 在 matchStr 中 对应索引 [ i - j , i - 1 ]
      • 在 patternStr 中 对应索引 [ 0 , j - 1 ]

      4️⃣ 那么我们再继续比较已经匹配的字符串后面的字符就可以了,即 判断 patternStr[i] 和 matchStr[j] 是否相等,这又回到了 步骤三!!!

      image-20221123201322516

  4. 判断 i 和 j 是否超出 各自的最大索引值

    • 如果都没到超出 各自最大索引值,就继续执行第三步进行比较(说明是个循环)
    • 如果至少有一个超出了 各自最大索引值,就退出循环
  5. 循环结束后,判断 j 是否超出了 模式字符串的最大索引值

    • 如果超出了,说明匹配成功,返回 patternStr 字符串匹配到的 matchStr 的第一个字符串的起始索引位置:i - j
    • 如果没有超出,说明匹配失败,返回 -1 表示没有匹配到

3.1.2 代码实现+测试

package kmp;

import java.util.Arrays;

/**
 * @author 狐狸半面添
 * @create 2022-11-22 22:43
 */
public class KMPAlgorithm {
    public static void main(String[] args) {
        String matchStr = "AABABADDABAC";
        String patternStr = "ABA";

        // 输出:index = 1
        System.out.println("index = " + kmpSearch(matchStr, patternStr, kmpNext(patternStr)));

    }

    /**
     * kmp搜索算法
     *
     * @param matchStr   原字符串
     * @param patternStr 子串
     * @param next       子串对应的部分匹配表
     * @return 如果是-1,就是没有匹配到,否则就返回第一个匹配的位置
     */
    public static int kmpSearch(String matchStr, String patternStr, int[] next) {

        int i = 0, j = 0;

        while (i < matchStr.length() && j < patternStr.length()) {

            if (matchStr.charAt(i) == patternStr.charAt(j)) {
                //相等就继续进行匹配
                i++;
                j++;

            } else {
                //如果 patternStr[i] 和 matchStr[j] 不相等

                if (j == 0) {
                    /*
                        表示 matchStr 没有匹配到 patternStr的第一个字符
                        那直接将 matchStr 的指针 i 向后移动一位即可
                     */
                    i++;
                } else {
                    j = next[j - 1];
                }
            }

        }
        return j == patternStr.length() ? i - j : -1;
    }

    /**
     * 获取一个字符串 pattern 的部分匹配表
     *
     * @param patternStr 用于模式匹配字符串
     * @return 存储部分匹配表的每个子串的最长公共前后缀的 next数组
     */
    public static int[] kmpNext(String patternStr) {
        //将 patternStr 转为 字符数组形式
        char[] patternArr = patternStr.toCharArray();

        //预先创建一个next数组,用于存储部分匹配表的每个子串的最长公共前后缀
        int[] next = new int[patternStr.length()];

        /*
            从第一个字符(对应索引为0)开始的子串,如果子串的长度为1,那么肯定最长公共前后缀为0
            因为这唯一的一个字符既是第一个字符,又是最后一个字符,所以前后缀都不存在 -> 最长公共前后缀为0
         */
        next[0] = 0;

        /*
          len有两个作用:
            1. 用于记录当前子串的最长公共前后缀长度
            2. 同时知道当前子串的最长公共前后缀的前缀字符串对应索引 [0,len-1]  <-- 可以拿示例分析一下
         */
        int len = 0;

        //从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度
        int i = 1;
        while (i < patternArr.length) {
            /*
                1.已经知道了上一个子串 对应索引[0,i-1] 的最长公共前后缀长度为 len
                  的前缀字符串是 索引[0,len-1],对应相等的后缀字符串是 索引[i-len,i-1]

                2.因此我们可以以 上一个子串的最长公共前后缀字符串 作为拼接参考
                  比较一下 patternArr[len] 与 patternArr[i] 是否相等
             */
            if (patternArr[len] == patternArr[i]) {
                /*
                    1.如果相等即 patternArr[len]==patternArr[i],
                      那么就可以确定当前子串的最长公共前后缀的
                      前缀字符串是 索引[0,len] ,对应相等的后缀字符串是 索引[i-len,i]

                    2.由于是拼接操作,那么当前子串的最长公共前后缀长度只需要在上一个子串的最长公共前后缀长度的基础上 +1 即可
                      即 next[i] = next[i-1] + 1 ,

                    3.由于 len 是记录的子串的最长公共前后缀长度,对于当前我们所在的代码位置而言
                      len 还是记录的上一个子串的最长公共前后缀长度,因此:
                      next[i] = next[i-1] + 1 等价于 next[i] = ++len
                 */

                // 等价于 next[i] = next[i-1] + 1
                next[i] = ++len;
                //既然找到了索引在[0,i]的子串的最长公共前后缀字符串长度,那就 i+1 去判断以下一个字符结尾的子串的最长公共前后缀长度
                i++;
            } else {
                /*
                    1.如果不相等 patternArr[len]!=patternArr[i]
                      我们想要求当前子串 对应索引[0,i] 的最长公共前后缀长度
                      我们就不能以 上一个子串的最长公共前后缀:前缀字符串pre  后缀字符串post (毫无疑问pre==post) 作为拼接参考

                    2.但可以思考一下:
                      pre的最长公共前缀字符串: 索引 [      0        , next[len-1] )
                      是等于
                      post的最长公共后缀字符串:索引 [ i-next[len-1] ,     i       )

                      则我们 就以 pre的最长公共前缀字符串/post的最长公共后缀字符串 作为拼接参考
                      去判断 pre的最长公共前缀字符串的下一个字符patternArr[next[len-1]] 是否等于 post的最长公共后缀字符串的下一个字符patternArr[i]

                    3.在第 1,2 步分析的基础上
                      我们可以在判断出 patternArr[len]!=patternArr[i] 后,
                      不去执行第二步:patternArr[next[len-1]] 是否等于 patternArr[i],
                      可以先修改len的值:len = next[len-1],len就成了 pre的最长公共前缀字符串长度/post的最长公共后缀字符串长度,
                      修改完之后,再去判断下一个字符 是否相等,即 判断 patternArr[len] 是否等于 patternArr[i]
                      仔细观察,这不又是在判断 这个循环中 if-else 语句吗

                    4.关于 len 这个值,在循环开始时我们解释的是:上一个子串的最长公共前后缀字符串的长度
                      但实际上我们在这里改为 len = next[len-1] 表示上一个子串的最长公共前后缀字符串的最长公共前后缀字符串的长度
                      是没有问题的,等价于上一个子串的较小的公共前后缀字符串。
                      既然进入了 else 语句说明字符不相等,就不能以 上一个子串的最长公共前后缀字符串 作为 拼接参考,就应当去缩小参考范围。
                 */
                if (len == 0) {

                    /*
                        len为0说明上一个子串已经没有了公共前后缀字符串
                        则我们没有继续寻找的必要 --> 索引在[0, i]的当前子串的最长公共前后缀字符串长度就是0
                     */
                    next[i] = len;

                    //继续寻找下一个字符串的最长公共前后缀字符串长度
                    i++;

                } else {
                    len = next[len - 1];
                }

            }
        }

        return next;
    }
}

3.2 允许匹配多个,可重复索引字符的代码

3.2.1 KMP算法的大致步骤

  1. 求出模式字符串patternStr的部分匹配表,已知待匹配的字符串 matchStr
  2. 定义两个指针 ij,分别指向 patternStr 和 matchStr ,初始化为0
  3. 定义一个 ArrayList 集合 firstIndexList,用于存储每次匹配成功的字符串的开始索引位置
  4. 判断 patternStr[i] 和 matchStr[j] 是否相等
    • 如果相等,则继续向后匹配:i++, j++
    • 如果不相等,则 i 不变,调整 j 为 模式字符串pattern 上一个子串(索引 [ 0, j-1 ])的最长公共前缀字符串的下一个索引位置,该索引位置也是最长公共前缀/后缀字符串的长度:j = next[ j - 1 ]
  5. 判断 i 是否超出 最大索引值
    • 如果超出了 matchStr 的 最大索引值,就退出循环
  6. 判断 j 是否超出了 最大索引值
    • 如果超出了 patternStr 的最大索引值:
      • 将匹配到的字符串的开始索引位置加入到 firstIndexList 集合:firstIndexList.add( i - j )
      • 调整 j 为 模式字符串pattern (索引 [ 0, j-1 ])的最长公共前缀字符串的下一个索引位置,该索引位置也是最长公共前缀/后缀字符串的长度:j = next[ j - 1 ]
  7. 第五步成立则循环退出,返回 firstIndexList 集合

3.2.2 代码实现+测试

package kmp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author 狐狸半面添
 * @create 2022-11-22 22:43
 */
public class KMPAlgorithm {
    public static void main(String[] args) {
        String matchStr = "AABABADDABAC";
        String patternStr = "ABA";

        // 输出:[1, 3, 8]
        System.out.println(kmpSearch(matchStr, patternStr, kmpNext(patternStr)).toString());

    }

    /**
     * kmp搜索算法
     *
     * @param matchStr   原字符串
     * @param patternStr 子串
     * @param next       子串对应的部分匹配表
     * @return 每次匹配成功的字符串的开始索引位置的集合
     */
    public static ArrayList<Integer> kmpSearch(String matchStr, String patternStr, int[] next) {

        int i = 0, j = 0;

        ArrayList<Integer> firstIndexList = new ArrayList<>();

        while (i < matchStr.length()) {

            if (matchStr.charAt(i) == patternStr.charAt(j)) {
                //相等就继续进行匹配
                i++;
                j++;

            } else {
                //如果 patternStr[i] 和 matchStr[j] 不相等

                if (j == 0) {
                    /*
                        表示 matchStr 没有匹配到 patternStr的第一个字符
                        那直接将 matchStr 的指针 i 向后移动一位即可
                     */
                    i++;
                } else {
                    j = next[j - 1];
                }
            }

            if (j == patternStr.length()) {
                //超出了最大索引值
                firstIndexList.add(i - j);
                j = next[j - 1];
            }

        }
        return firstIndexList;
    }

    /**
     * 获取一个字符串 pattern 的部分匹配表
     *
     * @param patternStr 用于模式匹配字符串
     * @return 存储部分匹配表的每个子串的最长公共前后缀的 next数组
     */
    public static int[] kmpNext(String patternStr) {
        //将 patternStr 转为 字符数组形式
        char[] patternArr = patternStr.toCharArray();

        //预先创建一个next数组,用于存储部分匹配表的每个子串的最长公共前后缀
        int[] next = new int[patternStr.length()];

        /*
            从第一个字符(对应索引为0)开始的子串,如果子串的长度为1,那么肯定最长公共前后缀为0
            因为这唯一的一个字符既是第一个字符,又是最后一个字符,所以前后缀都不存在 -> 最长公共前后缀为0
         */
        next[0] = 0;

        /*
          len有两个作用:
            1. 用于记录当前子串的最长公共前后缀长度
            2. 同时知道当前子串的最长公共前后缀的前缀字符串对应索引 [0,len-1]  <-- 可以拿示例分析一下
         */
        int len = 0;

        //从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度
        int i = 1;
        while (i < patternArr.length) {
            /*
                1.已经知道了上一个子串 对应索引[0,i-1] 的最长公共前后缀长度为 len
                  的前缀字符串是 索引[0,len-1],对应相等的后缀字符串是 索引[i-len,i-1]

                2.因此我们可以以 上一个子串的最长公共前后缀字符串 作为拼接参考
                  比较一下 patternArr[len] 与 patternArr[i] 是否相等
             */
            if (patternArr[len] == patternArr[i]) {
                /*
                    1.如果相等即 patternArr[len]==patternArr[i],
                      那么就可以确定当前子串的最长公共前后缀的
                      前缀字符串是 索引[0,len] ,对应相等的后缀字符串是 索引[i-len,i]

                    2.由于是拼接操作,那么当前子串的最长公共前后缀长度只需要在上一个子串的最长公共前后缀长度的基础上 +1 即可
                      即 next[i] = next[i-1] + 1 ,

                    3.由于 len 是记录的子串的最长公共前后缀长度,对于当前我们所在的代码位置而言
                      len 还是记录的上一个子串的最长公共前后缀长度,因此:
                      next[i] = next[i-1] + 1 等价于 next[i] = ++len
                 */

                // 等价于 next[i] = next[i-1] + 1
                next[i] = ++len;
                //既然找到了索引在[0,i]的子串的最长公共前后缀字符串长度,那就 i+1 去判断以下一个字符结尾的子串的最长公共前后缀长度
                i++;
            } else {
                /*
                    1.如果不相等 patternArr[len]!=patternArr[i]
                      我们想要求当前子串 对应索引[0,i] 的最长公共前后缀长度
                      我们就不能以 上一个子串的最长公共前后缀:前缀字符串pre  后缀字符串post (毫无疑问pre==post) 作为拼接参考

                    2.但可以思考一下:
                      pre的最长公共前缀字符串: 索引 [      0        , next[len-1] )
                      是等于
                      post的最长公共后缀字符串:索引 [ i-next[len-1] ,     i       )

                      则我们 就以 pre的最长公共前缀字符串/post的最长公共后缀字符串 作为拼接参考
                      去判断 pre的最长公共前缀字符串的下一个字符patternArr[next[len-1]] 是否等于 post的最长公共后缀字符串的下一个字符patternArr[i]

                    3.在第 1,2 步分析的基础上
                      我们可以在判断出 patternArr[len]!=patternArr[i] 后,
                      不去执行第二步:patternArr[next[len-1]] 是否等于 patternArr[i],
                      可以先修改len的值:len = next[len-1],len就成了 pre的最长公共前缀字符串长度/post的最长公共后缀字符串长度,
                      修改完之后,再去判断下一个字符 是否相等,即 判断 patternArr[len] 是否等于 patternArr[i]
                      仔细观察,这不又是在判断 这个循环中 if-else 语句吗

                    4.关于 len 这个值,在循环开始时我们解释的是:上一个子串的最长公共前后缀字符串的长度
                      但实际上我们在这里改为 len = next[len-1] 表示上一个子串的最长公共前后缀字符串的最长公共前后缀字符串的长度
                      是没有问题的,等价于上一个子串的较小的公共前后缀字符串。
                      既然进入了 else 语句说明字符不相等,就不能以 上一个子串的最长公共前后缀字符串 作为 拼接参考,就应当去缩小参考范围。
                 */
                if (len == 0) {

                    /*
                        len为0说明上一个子串已经没有了公共前后缀字符串
                        则我们没有继续寻找的必要 --> 索引在[0, i]的当前子串的最长公共前后缀字符串长度就是0
                     */
                    next[i] = len;

                    //继续寻找下一个字符串的最长公共前后缀字符串长度
                    i++;

                } else {
                    len = next[len - 1];
                }

            }
        }

        return next;
    }
}

3.3 允许匹配多个,不可重复索引字符的代码

3.3.1 KMP算法的大致步骤

  1. 求出模式字符串patternStr的部分匹配表,已知待匹配的字符串 matchStr
  2. 定义两个指针 ij,分别指向 patternStr 和 matchStr ,初始化为0
  3. 定义一个 ArrayList 集合 firstIndexList,用于存储每次匹配成功的字符串的开始索引位置
  4. 判断 patternStr[i] 和 matchStr[j] 是否相等
    • 如果相等,则继续向后匹配:i++, j++
    • 如果不相等,则 i 不变,调整 j 为 模式字符串pattern 上一个子串(索引 [ 0, j-1 ])的最长公共前缀字符串的下一个索引位置,该索引位置也是最长公共前缀/后缀字符串的长度:j = next[ j - 1 ]
  5. 判断 i 是否超出 最大索引值
    • 如果超出了 matchStr 的 最大索引值,就退出循环
  6. 判断 j 是否超出了 最大索引值
    • 如果超出了 patternStr 的最大索引值:
      • 将匹配到的字符串的开始索引位置加入到 firstIndexList 集合:firstIndexList.add( i - j )
      • 设置 j = 0 开始重新匹配
  7. 第五步成立则循环退出,返回 firstIndexList 集合

3.3.2 代码实现+测试

package kmp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author 狐狸半面添
 * @create 2022-11-22 22:43
 */
public class KMPAlgorithm {
    public static void main(String[] args) {
        String matchStr = "AABABADDABAC";
        String patternStr = "ABA";

        // 输出:[1, 8]
        System.out.println(kmpSearch(matchStr, patternStr, kmpNext(patternStr)).toString());

    }

    /**
     * kmp搜索算法
     *
     * @param matchStr   原字符串
     * @param patternStr 子串
     * @param next       子串对应的部分匹配表
     * @return 每次匹配成功的字符串的开始索引位置的集合
     */
    public static ArrayList<Integer> kmpSearch(String matchStr, String patternStr, int[] next) {

        int i = 0, j = 0;

        ArrayList<Integer> firstIndexList = new ArrayList<>();

        while (i < matchStr.length()) {

            if (matchStr.charAt(i) == patternStr.charAt(j)) {
                //相等就继续进行匹配
                i++;
                j++;

            } else {
                //如果 patternStr[i] 和 matchStr[j] 不相等

                if (j == 0) {
                    /*
                        表示 matchStr 没有匹配到 patternStr的第一个字符
                        那直接将 matchStr 的指针 i 向后移动一位即可
                     */
                    i++;
                } else {
                    j = next[j - 1];
                }
            }

            if (j == patternStr.length()) {
                //超出了最大索引值
                firstIndexList.add(i - j);
                j = 0;
            }

        }
        return firstIndexList;
    }

    /**
     * 获取一个字符串 pattern 的部分匹配表
     *
     * @param patternStr 用于模式匹配字符串
     * @return 存储部分匹配表的每个子串的最长公共前后缀的 next数组
     */
    public static int[] kmpNext(String patternStr) {
        //将 patternStr 转为 字符数组形式
        char[] patternArr = patternStr.toCharArray();

        //预先创建一个next数组,用于存储部分匹配表的每个子串的最长公共前后缀
        int[] next = new int[patternStr.length()];

        /*
            从第一个字符(对应索引为0)开始的子串,如果子串的长度为1,那么肯定最长公共前后缀为0
            因为这唯一的一个字符既是第一个字符,又是最后一个字符,所以前后缀都不存在 -> 最长公共前后缀为0
         */
        next[0] = 0;

        /*
          len有两个作用:
            1. 用于记录当前子串的最长公共前后缀长度
            2. 同时知道当前子串的最长公共前后缀的前缀字符串对应索引 [0,len-1]  <-- 可以拿示例分析一下
         */
        int len = 0;

        //从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度
        int i = 1;
        while (i < patternArr.length) {
            /*
                1.已经知道了上一个子串 对应索引[0,i-1] 的最长公共前后缀长度为 len
                  的前缀字符串是 索引[0,len-1],对应相等的后缀字符串是 索引[i-len,i-1]

                2.因此我们可以以 上一个子串的最长公共前后缀字符串 作为拼接参考
                  比较一下 patternArr[len] 与 patternArr[i] 是否相等
             */
            if (patternArr[len] == patternArr[i]) {
                /*
                    1.如果相等即 patternArr[len]==patternArr[i],
                      那么就可以确定当前子串的最长公共前后缀的
                      前缀字符串是 索引[0,len] ,对应相等的后缀字符串是 索引[i-len,i]

                    2.由于是拼接操作,那么当前子串的最长公共前后缀长度只需要在上一个子串的最长公共前后缀长度的基础上 +1 即可
                      即 next[i] = next[i-1] + 1 ,

                    3.由于 len 是记录的子串的最长公共前后缀长度,对于当前我们所在的代码位置而言
                      len 还是记录的上一个子串的最长公共前后缀长度,因此:
                      next[i] = next[i-1] + 1 等价于 next[i] = ++len
                 */

                // 等价于 next[i] = next[i-1] + 1
                next[i] = ++len;
                //既然找到了索引在[0,i]的子串的最长公共前后缀字符串长度,那就 i+1 去判断以下一个字符结尾的子串的最长公共前后缀长度
                i++;
            } else {
                /*
                    1.如果不相等 patternArr[len]!=patternArr[i]
                      我们想要求当前子串 对应索引[0,i] 的最长公共前后缀长度
                      我们就不能以 上一个子串的最长公共前后缀:前缀字符串pre  后缀字符串post (毫无疑问pre==post) 作为拼接参考

                    2.但可以思考一下:
                      pre的最长公共前缀字符串: 索引 [      0        , next[len-1] )
                      是等于
                      post的最长公共后缀字符串:索引 [ i-next[len-1] ,     i       )

                      则我们 就以 pre的最长公共前缀字符串/post的最长公共后缀字符串 作为拼接参考
                      去判断 pre的最长公共前缀字符串的下一个字符patternArr[next[len-1]] 是否等于 post的最长公共后缀字符串的下一个字符patternArr[i]

                    3.在第 1,2 步分析的基础上
                      我们可以在判断出 patternArr[len]!=patternArr[i] 后,
                      不去执行第二步:patternArr[next[len-1]] 是否等于 patternArr[i],
                      可以先修改len的值:len = next[len-1],len就成了 pre的最长公共前缀字符串长度/post的最长公共后缀字符串长度,
                      修改完之后,再去判断下一个字符 是否相等,即 判断 patternArr[len] 是否等于 patternArr[i]
                      仔细观察,这不又是在判断 这个循环中 if-else 语句吗

                    4.关于 len 这个值,在循环开始时我们解释的是:上一个子串的最长公共前后缀字符串的长度
                      但实际上我们在这里改为 len = next[len-1] 表示上一个子串的最长公共前后缀字符串的最长公共前后缀字符串的长度
                      是没有问题的,等价于上一个子串的较小的公共前后缀字符串。
                      既然进入了 else 语句说明字符不相等,就不能以 上一个子串的最长公共前后缀字符串 作为 拼接参考,就应当去缩小参考范围。
                 */
                if (len == 0) {

                    /*
                        len为0说明上一个子串已经没有了公共前后缀字符串
                        则我们没有继续寻找的必要 --> 索引在[0, i]的当前子串的最长公共前后缀字符串长度就是0
                     */
                    next[i] = len;

                    //继续寻找下一个字符串的最长公共前后缀字符串长度
                    i++;

                } else {
                    len = next[len - 1];
                }

            }
        }

        return next;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/34675.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用koa框架部署项目

将打包好的代码打包上线&#xff0c;并在nodejs中代理跨域 前端工程师完成开发流程&#xff0c;按照常规的做法&#xff0c;此时&#xff0c;运维会将我们的代码部署到阿里云的ngix服务上&#xff0c;对于我们而言&#xff0c;我们可以将其部署到本机的nodejs环境中 企业代码部…

AI产业沙龙【语音之家】—京东在AI语音方向的技术探索与实践

由CCF语音对话与听觉专委会 、中国人工智能产业发展联盟&#xff08;AIIA&#xff09;评估组、京东科技、语音之家、希尔贝壳共同主办的【语音之家】AI产业沙龙—京东在AI语音方向的技术探索与实践&#xff0c;将于2022年11月29日19:30-21:30线上直播。 沙龙简介 如今人机对话…

【构建ML驱动的应用程序】第 10 章 :为模型构建安全措施

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

MYSQL进阶(3)

SQL语句的优化: 1)Insert语句的优化:我们如果说向数据库中插入数据&#xff0c;可以从下面几个方面来进行优化: 1.1)批量插入数据&#xff0c;values后面加上多个括号 1.2)手动控制事务&#xff0c;关闭自动提交&#xff0c;避免大量用到事务 1.3)按照主键顺序插入 2)大批量插入…

unity和C#的多线程 Thread的IsBackground属性,详解

Thread的IsBackground = true;//主程序结束后,线程随之结束 【日常要用到的】 Thread的IsBackground = false;//主程序结束后,线程不会随之结束 案例1、 using System; using System.Threading; namespace ConsoleApp1 { class Program { static void Main(st…

【数据结构】顺序表—纯C实现顺序表

顺序表文章目录定义特点缺陷静态顺序表动态顺序表接口实现顺序表初始化顺序表销毁顺序表增容头部的插入删除头插头删尾部的插入删除尾插尾删中间的插入删除中间插入中间删除顺序表查找顺序表打印定义 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xf…

sklearn.metrics模块重要API总结(持续更新)

目录前言各类指标分类指标&#xff08;Classification metrics&#xff09;sklearn.metrics.accuracy_scoresklearn.metrics.aucaverage_precision_score (AP)回归指标&#xff08;Regression metrics&#xff09;多标签排序指标&#xff08;Multilabel ranking metrics&#x…

JUC05-AQS、ReentrantLock原理

一、AQS 全称是 AbstractQueuedSynchronizer,是同步器的相关框架&#xff0c;juc中很多锁的实现类依赖同步器(AQS的子类)完成核心操作 要点 内部维护state变量资源状态&#xff0c;state0表示当前无线程占用&#xff0c;state!0代表该锁正在被线程占用提供FIFO的等待队列&…

Mongodb实验二——分片集群搭建

目录 前言 题目一 1.创建副本集实例&#xff08;Mongod&#xff09; cfg0_0配置 cfg0_1配置 cfg0_2配置 ch0_0配置 ch0_1配置 ch0_2配置 ch1_0配置 ch1_1配置 ch1_2配置 2.初始化副本集 ch0conf.js配置 ch1conf.js配置 3.创建路由节点&#xff08;分片管理服务器…

Live800:避开客服雷区,提升客服转化

小刘在某在线商城开了个小店铺&#xff0c;店铺做得不错&#xff0c;宝贝销量稳定&#xff0c;小日子过得有滋有味。 最近一段时间&#xff0c;流量持续下滑&#xff0c;销量跟着走低&#xff0c;可愁死了。 "流量有波动很正常&#xff0c;但是一直降一直降&#xff0c;肯…

《恋上数据结构与算法》第1季:算法概述

数据结构与算法的学习笔记目录&#xff1a;《恋上数据结构与算法》的学习笔记 目录索引算法概述1. 算法和数据结构1.1 什么是算法1.2 什么是数据结构2. 时间复杂度2.1 如何判断一个算法的好坏呢&#xff1f;2.2 基本操作执行次数2.3 大O表示法3. 空间复杂度3.1 概念定义4. 算法…

MCE | BCL6 小分子也能发挥类 PROTAC 的功能

图示摘要&#xff1a;来自作者 Jonas Koeppel (Ph. D., Department of Medical Oncology, Dana-Farber Cancer Institute) 的 Twitter杂合双功能降解技术 (PROTAC&#xff0c;靶蛋白配体-Linker-E3 连接酶配体组成的“三体”聚合物) 已被用于多种疾病相关靶点的降解剂开发&…

老年患者植入LVAD的挑战:胃肠道出血

EF值降低的心力衰竭随着年龄的增长越来越常见。据研究报道&#xff0c;75岁以上人群的患病率是普通人群的4倍&#xff0c;目前是老年人死亡和住院的最常见原因之一。治疗方案主要包括心脏移植(HT)和长期左心室辅助装置(LVAD)植入&#xff0c;这两种方法已被临床研究证明可降低患…

Flutter高仿微信-第34篇-单聊-小视频

Flutter高仿微信系列共59篇&#xff0c;从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。 详情请查看 效果图&#xff1a; 详情请参考 Flutter高仿微信-第29篇-单聊 &#xff0c; 这里只是提取小视频的部…

Android App开发即时通信中通过SocketIO在客户端与服务端间传输文本和图片的讲解及实战(超详细 附源码)

需要源码和服务端代码请点赞关注收藏后评论区留下QQ~~~ 一、通过SocketIO传输文本消息 虽然HTTP协议能够满足多数常见的接口交互&#xff0c;但是他属于短连接&#xff0c;每次调用完就自动断开连接&#xff0c;并且HTTP协议区分了服务端和客户端&#xff0c;双方的通信过程是…

机器学习知识经验分享之三:基于卷积神经网络的经典目标检测算法

文章目录前言一、一阶段目标检测算法1.YOLO系列算法2.SSD检测算法3. RetinaNet检测算法二、两阶段目标检测算法1.Faster R-CNN检测算法2.Mask R-CNN检测算法3.Cascade R-CNN检测算法总结前言 本系列文章将对机器学习知识进行分享总结。便于大家从理论层面了解人工智能基础原理…

软件被人后台篡改了收款码属于入侵吗?

最近很多做平台的小伙伴&#xff0c;碰到了同样的问题&#xff0c;就是软件程序后台被恶意篡改收款二维码 这个问题出现在平台主身上无疑是雪上加霜&#xff0c;第一时间找到了小蚁君&#xff0c;分析了一下当时的情况&#xff0c;先安装了小蚁的入侵检测系统&#xff0c;显示…

计算机毕业设计之java+ssm协同办公系统

项目介绍 本公司文档协同办公管理系统采用SSM&#xff08;SpringSpringMVCMyBatis&#xff09;框架开发,主要包括系统用户管理模块、用户信息模块、文件信息管理、个人事务管理、资料信息管理、登录模块、和退出模块等多个模块. 本系统主要包含了等系统用户管理、用户信息管理…

webpack5 PWA解决Web App 项目网络离线情况没法访问情况

为什么 开发 Web App 项目&#xff0c;项目一旦处于网络离线情况&#xff0c;就没法访问了。 我们希望给项目提供离线体验。 是什么 渐进式网络应用程序(progressive web application - PWA)&#xff1a;是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。…

Go语言中操作Redis

Redis介绍 Redis是一个开源的内存数据库&#xff0c;Redis提供了多种不同类型的数据结构&#xff0c;很多业务场景下的问题都可以很自然地映射到这些数据结构上。 除此之外&#xff0c;通过复制、持久化和客户端分片等特性&#xff0c;我们可以很方便地将Redis扩展成一个能够包…