KMP算法是一种高效的字符串匹配算法
解决了暴力算法冗杂繁琐的特点
将时间复杂度从n^2优化到了n+m(子串和模式串长度)

其核心思想是:匹配失配时,模式串指针不回退到起点,而是根据预处理的Next数组回退到「合适位置」,跳过已匹配的有效部分,避免重复比较。

一、回退机制

你有两个字符串

  • 主串(Text, S):ABABABCABA
  • 模式串(Pattern, P):ABABC
  • 匹配过程:二者逐字符匹配,当匹配到模式串的C时,对应主串的第三个A,此时失配;已成功匹配的字符为ABAB。
  • 优化操作:找到已匹配部分ABAB的最长相等前后缀(AB),让模式串的前缀AB直接匹配主串的后缀AB,模式串指针回退到该前缀的末尾,主串指针不回退,继续向后匹配。
    总而言之就是
    当失配的时候 用模式串的前缀末尾继续去匹配子串的后缀末尾
关键概念
  1. 前缀:字符串中除最后一个字符外,所有以第一个字符开头的连续子串(如ABAB的前缀:A、AB、ABA);
  2. 后缀:字符串中除第一个字符外,所有以最后一个字符结尾的连续子串(如ABAB的后缀:B、AB、BAB);
  3. 最长公共前后缀:前缀和后缀中长度最长且内容完全相等的子串(如ABAB的最长公共前后缀为AB)。
二、Next数组

KMP的回退机制完全由Next数组实现,Next数组是为模式串单独构建的数组,与模式串长度一致,数组中每个位置的值对应:模式串当前位置字符失配时,指针应回退的目标索引(本质是模式串前i个字符的最长公共前后缀长度)。
Next数组长度与模式串一一对应 除了第一个值进行的初始化,
其他元素例存储了当前对应长度的最长公共前后缀的大小,也就是失配后应该跳转的索引

在这里插入图片描述

简单来说:Next[i] 表示模式串前i+1个字符的最长公共前后缀的长度,也是失配后模式串指针应回退到的位置。

三、KMP算法实现(Java)

KMP算法分为两个核心步骤:构建Next数组、利用Next数组进行主串与模式串的匹配,整体采用双指针法实现,且构建Next数组的过程也复用了KMP的核心思想(模式串自匹配)。

步骤1:构建Next数组

核心逻辑
用双指针分别指向模式串的前缀末尾(j)和后缀末尾(i),通过自匹配找到每个位置的最长公共前后缀长度,填充Next数组:

  • j:既代表前缀末尾位置,也代表当前已匹配的最长公共前后缀长度,初始为0;
  • i:代表后缀末尾位置,从1开始遍历(第一个字符无前后缀,Next[0]固定为0);
  • 失配回退:前后缀字符不匹配时,j根据已构建的Next数组回退一格(j = next[j-1]),直到j=0或字符匹配;
  • 匹配更新:前后缀字符匹配时,j自增(最长公共前后缀长度+1),并将j赋值给Next[i]。

回退机制和KMP算法的后退机制是一样的,本质上就是通过营造时间差
自己和自己匹配
这个因为在构造Next数组中的时候,前面的Next数组已经构建完毕了,这样就可以使用该思想了

代码实现

/**
 * 构建模式串的Next数组
 * @param pattern 模式串
 * @return 对应Next数组
 */
public static int[] getNext(String pattern) {
    int m = pattern.length();
    int[] next = new int[m];
    next[0] = 0; // 第一个字符无前后缀,最长公共前后缀长度为0
    
    // j:前缀末尾/已匹配长度,i:后缀末尾
    for (int i = 1, j = 0; i < m; i++) {
        // 失配回退:利用已构建的Next数组,回退到合适位置
        while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
            j = next[j - 1];
        }
        // 匹配成功:最长公共前后缀长度+1
        if (pattern.charAt(i) == pattern.charAt(j)) {
            j++;
        }
        // 填充当前位置的Next值
        next[i] = j;
    }
    return next;
}
步骤2:利用Next数组进行字符串匹配

核心逻辑
用双指针分别指向主串(i)和模式串(j),逐字符匹配,失配时通过Next数组让模式串指针回退,主串指针始终不回退:

  • i:主串遍历指针,从0开始,一直向后遍历不回退;
  • j:模式串匹配指针,从0开始,匹配成功则后移,失配则根据Next数组回退;
  • 失配处理:主串与模式串字符不匹配时,j回退(j = next[j-1]),直到j=0或字符匹配;
  • 匹配成功判定:当j等于模式串长度时,说明模式串完全匹配,返回主串中匹配的起始索引;
  • 边界检查:提前判断模式串为空、主串长度小于模式串的情况,直接返回对应结果。
代码实现
import java.util.Arrays;
/**
 * KMP核心匹配方法
 * @param text 主串
 * @param pattern 模式串
 * @return 模式串在主串中首次匹配的起始索引,未找到返回-1
 */
public static int kmpSearch(String text, String pattern) {
    // 边界检查:模式串为空直接返回0,主串更短直接返回-1
    if (pattern == null || pattern.length() == 0) return 0;
    if (text == null || text.length() < pattern.length()) return -1;

    int n = text.length(); // 主串长度
    int m = pattern.length(); // 模式串长度
    int[] next = getNext(pattern); // 获取模式串的Next数组
    // System.out.println("Next数组:" + Arrays.toString(next)); // 调试用,打印Next数组

    // i:主串指针,j:模式串指针
    for (int i = 0, j = 0; i < n; i++) {
        // 失配回退:利用Next数组跳过重复匹配部分
        while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
            j = next[j - 1];
        }
        // 匹配成功:模式串指针后移
        if (text.charAt(i) == pattern.charAt(j)) {
            j++;
        }
        // 完全匹配:返回起始索引(当前主串位置 - 模式串长度 + 1)
        if (j == m) {
            return i - m + 1;
            // 若需查找所有匹配位置,注释上方return,执行以下代码:
            // j = next[j - 1]; // 回退继续匹配
            // res.add(i - m + 1); // 收集起始索引
        }
    }
    return -1; // 未找到匹配
}
四、关键细节总结
  1. Next数组的本质:模式串前i+1个字符的最长公共前后缀长度,直接决定了失配后模式串指针的回退位置;
  2. 双指针的核心作用:无论是构建Next数组还是正式匹配,双指针都实现了「一次遍历,不重复比较」,是时间复杂度优化到O(n+m)的关键;
  3. 回退的核心原则:失配时主串指针不回退,仅模式串指针根据Next数组回退,跳过已验证的有效匹配部分;
  4. 自匹配特性:构建Next数组时,模式串相当于自己和自己匹配,复用了KMP的失配回退逻辑,无需额外嵌套遍历。
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐