KMP算法——通俗易懂讲好KMP算法:实例图解分析+详细代码注解

news2025/4/19 13:21:56

文章目录

  • 1.kmp算法基本介绍
  • 2.字符串的最长公共前后缀&部分匹配表
    • 2.1 什么是最长公共前后缀
    • 2.2 什么是部分匹配表Next
    • 2.3 字符串最长公共前后缀&部分匹配表的代码实现
    • 2.4 代码测试
  • 3.根据部分匹配表搜索字符串匹配位置
    • 3.1 匹配成功一个就退出匹配的代码
      • 3.1.1 KMP算法的大致步骤
      • 3.1.2 代码实现+测试
    • 3.2 允许匹配多个,可重复索引字符的代码
      • 3.2.1 KMP算法的大致步骤
      • 3.2.2 代码实现+测试
    • 3.3 允许匹配多个,不可重复索引字符的代码
      • 3.3.1 KMP算法的大致步骤
      • 3.3.2 代码实现+测试

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 ]
      1. 由于 完全相等,则 str1 的 最长公共后缀字符串 一定等于 str2 的 最长公共前缀字符串,那么:
      • 将 i 定位到 str1 的 最长公共后缀字符串 的 下一个字符位置,但很明显,i 此时的位置肯定已经是在 str1 的最长公共后缀字符串的下一个字符的位置,因此 i 的值不需要做调整

      • 将 j 定位到 str2 的 最长公共前缀字符串 的 下一个字符位置next [ j - 1] 不仅代表 上一个子串str1的最长公共前后缀字符串长度,也是最长公共前缀字符串的下一个字符的索引。因此,我们只需要:j = next[ j - 1 ]

      1. 修改完毕,我们此时已经匹配的字符串:
      • 在 matchStr 中 对应索引 [ i - j , i - 1 ]
      • 在 patternStr 中 对应索引 [ 0 , j - 1 ]
      1. 那么我们再继续比较已经匹配的字符串后面的字符就可以了,即 判断 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/29532.html

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

相关文章

Vue父组件给子组件传参数

别人在调用我们写的组件时&#xff0c;虽然要实现的结构一样&#xff0c;但如果别人想改一下显示的内容或者之类的&#xff0c;该怎么做呢&#xff1b;这时候就要提到“传参数”这个词了&#xff0c;别人可以通过传不同的参数&#xff0c;来实现他们具体的结构&#xff1b; 传参…

SpringBoot SpringBoot 开发实用篇 5 整合第三方技术 5.22 RabbitMQ 安装

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇5 整合第三方技术5.22 RabbitMQ 安装5.22.1 Erlang下载5.22.2 安装5.…

HTML+CSS期末大作业 中国传统美食网站设计 节日美食13页 html5网页设计作业代码 html制作网页案例代码 html大作业网页代码

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

3招学会TikTok电商选品,速看

Sensor Tower商店情报数据显示&#xff0c;2022年10月Instagram以将近6700万下载量&#xff0c;成为全球移动应用&#xff08;非游戏&#xff09;下载榜冠军&#xff0c;较2021年10月增长17.2%。其中&#xff0c;印度市场的下载量占45.2%&#xff0c;美国市场的下载量占比为5.4…

《码出高效:Java开发手册》笔记之二-面向对象

前言 第二章主要是讲面向对象&#xff0c;也就是oop&#xff0c;这个概念其实很多人已经听腻了&#xff0c;都是非常基础的知识&#xff0c;本章就是讲一些java以及很多编程语言的基础设计思想 正文 oop理念 面向对象是在早期滥用面向过程编程后出现的&#xff0c;面向过程…

CSS清除浮动的五种方法(超详细)

1.为什么要清除浮动? 浮动的原理是让图片脱离文档流&#xff0c;直接浮在桌面上。我们一般布局的时候都是只设置宽度不设置高度&#xff0c;让内容来自动填充高度。但使用浮动后会让原本填充的高度消失&#xff0c;父元素高度为0&#xff0c;后续添加内容布局会产生混乱,造成…

C语言tips-数组指针和指针数组

最近因为工作需要开始重新学c语言&#xff0c;越学越发现c语言深不可测&#xff0c;当初用python轻轻松松处理的一些数据&#xff0c;但是c语言写起来却异常的复杂&#xff0c;这个板块就记录一下我的c语言复习之路 数组指针 概念&#xff1a;顾名思义就是一个指针&#xff0c;…

英特尔oneAPI-用于异构计算的英特尔oneAPI

文章目录前景解决方案CUDA替代方案OpenMPOpenACCC 库Python 和 JavaSYCL 和 oneAPI结论如今&#xff0c;异构性广泛存在于高性能计算和消费电子产品中。这些系统在传统 CPU 的基础上增加了大量协处理器或加速器&#xff0c;例如 GPU、TPU 和 FPGA。然而&#xff0c;没有一种简单…

梯度下降——机器学习

一、实验内容 掌握基于密度的聚类方法的基本思想&#xff1b;掌握单变量函数的梯度下降的原理、算法及python实现&#xff1b;掌握双变量函数的梯度下降的原理、算法及python实现&#xff0c;并测试分析&#xff1b;理解学习率η的选择并测试分析。 二、实验过程 1、算法思想 在…

状态估计|基于 MMSE 的分析估计器的不确定电力系统分析(Matlab代码实现)

一、概述 在分布式电网系统中部署可再生资源带来了一系列新挑战&#xff0c;主要是由于它们的可变性和对气候参数的依赖性&#xff0c;这可能对测量潮流和状态估计所需的系统参数产生重大影响。第一个旨在根据某些参数的先验知识&#xff08;或预测&#xff09;计算整个系统参…

从头开始进行CUDA编程:流和事件

前两篇文章我们介绍了如何使用GPU编程执行简单的任务&#xff0c;比如令人难以理解的并行任务、使用共享内存归并&#xff08;reduce&#xff09;和设备函数。为了提高我们的并行处理能力&#xff0c;本文介绍CUDA事件和如何使用它们。但是在深入研究之前&#xff0c;我们将首先…

C++【智能指针】

文章目录一、什么是智能指针RAII思想std::auto_ptr二、智能指针的拷贝问题&#xff08;C98&#xff09;1.unique_ptr2.shared_ptrshared_ptr的问题循环引用的问题3.weak_ptr内存泄漏的危害一、什么是智能指针 #include<iostream>using namespace std; int div() {int a,…

哈希散列表hlist_head - linux内核经典实例

hlist_head和hlist_node用于散列表&#xff0c;分别表示列表头&#xff08;数组中的一项&#xff09;和列表头所在双向链表中的某项&#xff0c;两者结构如下: include/linux/types.h(line 190) struct hlist_head {struct hlist_node *first; };struct hlist_node {struct h…

护眼灯真的可以保护眼睛吗?2022双十二选哪款护眼灯对孩子眼睛好

传统的台灯只是单一色光&#xff0c;无法调节台灯的照度和色温&#xff0c;长时间使用不但不可以护眼&#xff0c;而且还会导致近视、散光等各种问题的发生。现在的护眼台灯大多都是使用led灯珠作为发光源&#xff0c;不但本身比较高效节能&#xff0c;而且光线可调控&#xff…

react--redux

此篇文章非学习使用&#xff0c;学习勿入 redux 文档&#xff1a; http://www.redux.org.cn 用于做状态管理的js库 集中管理react中多个组件共享的状态 安装&#xff1a; cnpm i redux 给形参赋值&#xff0c;表示形参的默认值 错误&#xff1a; 对象不能作为一个dom元素…

NPDP认证|制造业产品经理日常工作必备技能,快来学习提升吧!

不同阶段的产品经理对技能的掌握程度要求不同&#xff0c;侧重点也不同&#xff0c;一般包括需求分析、数据分析、竞品分析、商业分析、行业分析、需求收集、产品设计、版本管理、用户调研等。这些技能&#xff0c;是我们必须要掌握的专业技能。 比如&#xff1a;对于刚入行的…

异常检测算法分类总结(含常用开源数据集)

作者&#xff1a;云智慧算法工程师 Chris Hu 异常检测是识别与正常数据不同的数据&#xff0c;与预期行为差异大的数据。本文详细介绍了异常检测的应用领域以及总结梳理了异常检测的算法模型分类。文章最后更是介绍了常用的异常算法数据集。 异常的概念与类型 目前异常检测主…

硝酸根离子深度去除树脂

普通的阴离子交换树脂对阴离子的交换次序是&#xff1a;SO42-&#xff1e;NO3-&#xff1e;HCO3-&#xff0c;对硝酸盐没有选择性&#xff0c;优先交换水中硫酸根&#xff0c;造成树脂再生频繁&#xff0c;产水中氯离子含量增高&#xff0c;出水水质稳定性差&#xff0c;树脂交…

[注塑]各种进胶方式优缺点分析

[注塑]各种进胶方式优缺点分析1.直接进胶2.测胶口3.搭接式浇口4.扇形浇口5.潜胶6.弧线浇口7.针形浇口结构设计的时&#xff0c;分析浇口的进胶方式尤为重要&#xff0c;为了简便我们的设计&#xff0c;常常需要将一些常用的标准形式&#xff0c;以下是我们常见的一些浇口形式。…

死磕sparkSQL源码之TreeNode

InternalRow体系 学习TreeNode之前&#xff0c;我们先了解下InternalRow。 对于我们一般接触到的数据库关系表来说&#xff0c;我们对于数据库中的数据操作都是按照“行”为单位的。在spark sql内部实现中&#xff0c;InternalRow是用来表示这一行行数据的类。看下源码中的解…