北风toto头像

KMP 算法详解:从暴力匹配到 O(n+m) 的字符串搜索

KMP 算法详解:从暴力匹配到 O(n+m) 的字符串搜索

字符串匹配是算法面试和工程实践中非常经典的问题:给定一个主串 S 和一个模式串 P,如何在 S 中找到 P 第一次出现的位置?

最直观的做法是暴力匹配,但它在最坏情况下会达到 O(n×m)。KMP 算法通过预处理模式串,把时间复杂度优化到 O(n+m),并且主串指针全程不回退。

本文从暴力匹配的问题出发,逐步拆解 KMP 的核心思想、next 数组的构建过程以及完整实现。

一、暴力匹配的问题

暴力匹配的思路很简单:从主串的每一个位置开始,逐个字符与模式串比较。如果失配,就把主串指针回退到上一次起点的下一位,模式串指针重置为 0。

例如:

主串 S:A B A B A B C
模式串 P:A B A B C

前四个字符 ABAB 都匹配成功,第 5 个字符失配。暴力做法会把主串指针回退,重新从下一个位置开始比较。

但这里有一个问题:前面已经匹配成功的 ABAB 中,开头 AB 和结尾 AB 是相同的。也就是说,主串中已经走过的部分其实包含了一些“可复用信息”。暴力匹配没有利用这一点,导致大量重复比较。

二、KMP 的核心思想

KMP 的关键在于两件事:

  1. 主串指针不回退;
  2. 失配时,模式串指针根据已匹配信息跳转到合适位置。

为了实现第二点,我们需要先对模式串做预处理,得到一张“跳转表”,也就是常说的 next 数组。

三、理解最长相等前后缀

next 数组的基础概念是最长相等前后缀。

  • 前缀:字符串中不包含最后一个字符的头部连续子串。
  • 后缀:字符串中不包含第一个字符的尾部连续子串。

以模式串 ABABC 为例:

子串最长相等前后缀长度
A无0
AB无0
ABAA1
ABABAB2
ABABC无0

所以它的前缀表为:

[0, 0, 1, 2, 0]

这个数组的含义是:对于模式串的前 i+1 个字符,它的最长相等前后缀长度是多少。

四、next 数组到底在干什么?

假设当前模式串已经匹配到第 j 个字符时失配,说明 P[0..j-1] 已经和主串中的某段完全匹配。

此时我们不需要从头开始,只需要知道:在这段已匹配的字符串中,有多长的后缀恰好等于模式串的前缀?

这个长度就是 next[j-1]。

因为主串中这段后缀已经匹配成功,而它又等于模式串的前缀,所以模式串可以直接把前缀对齐到这段后缀上,继续比较即可。

换句话说,next 数组回答了一个问题:

失配后,模式串最多能向右滑动多少,同时保证已经匹配的部分不丢失?

五、next 数组的构建过程

构建 next 数组,本质上是让模式串和自己做一次匹配。

我们维护两个指针:

  • i:当前正在计算的后缀末尾位置;
  • j:当前已知的最长相等前后缀长度,也指向前缀末尾。

初始时:

next[0] = 0;
j = 0;

然后从 i = 1 开始遍历模式串:

for i from 1 to m - 1:
    while j > 0 and P[i] != P[j]:
        j = next[j - 1];
    if P[i] == P[j]:
        j++;
    next[i] = j;

这里的 while 循环是重点。当 P[i] != P[j] 时,不能直接把 j 设为 0,因为更短的前后缀可能仍然匹配。所以让 j 回退到 next[j-1],继续尝试。

以模式串 ABABC 为例:

  1. i=1, j=0:B != A,next[1]=0。
  2. i=2, j=0:A == A,j=1,next[2]=1。
  3. i=3, j=1:B == B,j=2,next[3]=2。
  4. i=4, j=2:C != A,j=next[1]=0;C != A,next[4]=0。

最终得到:

next = [0, 0, 1, 2, 0]

六、主串匹配过程

有了 next 数组后,主串匹配就非常清晰了。

int i = 0; // 主串指针
int j = 0; // 模式串指针

while (i < n) {
    if (S.charAt(i) == P.charAt(j)) {
        i++;
        j++;
    } else if (j > 0) {
        j = next[j - 1];
    } else {
        i++;
    }

    if (j == m) {
        return i - m; // 匹配成功
    }
}
return -1; // 未找到

整个过程只有三种情况:

  1. 当前字符匹配:i++,j++;
  2. 失配但 j > 0:模式串指针回退到 next[j-1];
  3. 失配且 j == 0:主串指针 i++。

由于主串指针 i 只会向后移动,不会回退,所以主串每个字符最多被访问常数次。

七、复杂度分析

指标复杂度
时间O(n + m)
空间O(m)

其中 n 是主串长度,m 是模式串长度。预处理模式串需要 O(m),主串匹配需要 O(n)。

八、完整代码示例

下面给出一个基于 next 数组的 Java 实现:

public class KMP {

    public static int strStr(String haystack, String needle) {
        if (needle.isEmpty()) return 0;

        int n = haystack.length();
        int m = needle.length();

        int[] next = buildNext(needle);

        int i = 0;
        int j = 0;

        while (i < n) {
            if (haystack.charAt(i) == needle.charAt(j)) {
                i++;
                j++;
            } else if (j > 0) {
                j = next[j - 1];
            } else {
                i++;
            }

            if (j == m) {
                return i - m;
            }
        }
        return -1;
    }

    private static int[] buildNext(String pattern) {
        int m = pattern.length();
        int[] next = new int[m];
        int j = 0;

        for (int i = 1; i < m; i++) {
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }
            if (pattern.charAt(i) == pattern.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }
}

九、常见误区

1. next 数组定义不统一

不同资料对 next 数组的定义可能不同:

  • 有的从 0 开始,表示最长相等前后缀长度;
  • 有的从 -1 开始,表示失配后的跳转位置;
  • 还有 nextval 优化,用于减少模式串内部重复字符带来的无效比较。

理解时不需要死记下标偏移,只要抓住本质:失配后,模式串指针应该跳到哪里,才能复用已匹配信息。

2. 误以为主串指针也要回退

KMP 的优势正是主串指针不回退。模式串指针根据 next 数组跳转,主串指针始终向右移动。

3. 把前缀表理解成“相同子串个数”

最长相等前后缀长度并不是统计有多少个相同子串,而是找最长的、既是前缀又是后缀的子串长度。

十、总结

KMP 算法并不复杂,它的核心只有三步:

  1. 理解最长相等前后缀;
  2. 用模式串自身构造 next 数组;
  3. 匹配时主串指针不回退,模式串指针按 next 数组跳转。
  4. 口语化表述:“以当前位置结尾的后缀”和“从第一个字母开始的前缀”重合的长度
  5. “以当前位置结尾的后缀”:不能包含第一个字母,同时要包含当前位置的字母。

它把字符串匹配从“每次失配都重来”优化为“尽可能复用已匹配信息”,从而实现了 O(n+m) 的线性时间复杂度。

掌握 KMP 后,也可以进一步学习 BM 算法、Sunday 算法等更高效的字符串匹配方法。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/m0_46085118/article/details/167589325

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--