KMP算法详解 -- 串的模式匹配
学数据结构时, 参考课本无法让我理解模式匹配的 KMP 算法, 在和AI探讨之后遂有所领悟 , 把我的理解与心得写在下面.
KMP算法 -- next数组超详细解析
一句话概括: 把next数组理解为"当在这里失败时,我知道前面有一段是重复的,所以可以直接跳到重复段开始的地方继续匹配"
1. 为什么需要next数组?
暴力匹配的问题:
// 暴力匹配示例
主串: a b a b a b c a
模式串: a b a b c
↑ ↑ ↑ ↑ ×当匹配到第5个字符时失败,暴力算法会:
主串回退到第2个字符'b'
模式串回退到开头
重新开始匹配
问题:主串指针回退,效率低!
2. next数组的核心思想
核心洞察:匹配失败时,利用已经匹配成功的信息,让模式串"智能跳转",主串指针永不回退!
3. next数组到底是什么?
3.1 基本定义
对于模式串中的每个位置j,
next[j]
表示:当模式串第j个字符匹配失败时,应该跳转到模式串的哪个位置继续匹配
3.2 更直观的理解
next[j]
= 模式串前j个字符组成的子串中,最长相等前后缀的长度什么是前后缀?
前缀:包含首字符但不包含尾字符的所有连续子串
后缀:包含尾字符但不包含首字符的所有连续子串
4 .手动计算next数组(重点)!
- 让我们用模式串
"ababc"
来一步步计算: 步骤1:列出所有子串并找出最长相等前后缀
位置j 子串(0到j-1) 所有前缀 所有后缀 最长相等前后缀 next[j] 0 (空串) 无 无 无 -1 1 "a" 无 无 无 0 2 "ab" "a" "b" 无 0 3 "aba" "a","ab" "a","ba" "a" (长度1) 1 4 "abab" "a","ab","aba" "b","ab","bab" "ab" (长度2) 2 - 所以
next = [-1, 0, 0, 1, 2]
步骤2:验证理解
- 让我们手动模拟匹配过程:
- 主串: a b a b a b c a
模式串: a b a b c
0 1 2 3 4 ← 位置索引
next: -1 0 0 1 2 - 位置0-3匹配成功,位置4失败(c≠a)
此时j=4, next[4]=2
模式串跳转到位置2继续匹配:
a b a b c
a b a b c ← 跳转后 - 为什么能这样跳转?
- 因为前4个字符"abab"中,最长相等前后缀是"ab",所以我们可以直接把前缀"ab"对齐到后缀"ab"的位置!
- 让我们用模式串
5 算法构建next数组的原理
5.1 核心观察
如果我们已经知道
next[j] = k
,那么:如果
pattern[j] == pattern[k]
,则next[j+1] = k + 1
如果不等,就找更短的相等前后缀:
k = next[k]
5.2 详细推导过程
- 还是以
"ababc"
为例: 初始化: next[0] = -1, j=0, k=-1
循环开始:
1. j=0, k=-1 → k==-1 → j=1, k=0, next[1]=0
解释:位置1前面只有"a",没有相等前后缀2. j=1, k=0 → pattern[1]='b' ≠ pattern[0]='a'
→ k = next[0] = -1
→ k==-1 → j=2, k=0, next[2]=0
解释:位置2前面是"ab",没有相等前后缀3. j=2, k=0 → pattern[2]='a' == pattern[0]='a'
→ j=3, k=1, next[3]=1
解释:位置3前面是"aba",最长相等前后缀是"a"(长度1)4. j=3, k=1 → pattern[3]='b' == pattern[1]='b'
→ j=4, k=2, next[4]=2
解释:位置4前面是"abab",最长相等前后缀是"ab"(长度2)
- 还是以
5.3 可视化理解
前后缀匹配的直观展示
- 对于模式串
"ababc"
: 位置3的分析:
子串: a b a
前缀: a, ab
后缀: a, ba
相等前后缀: "a" ← 所以next[3]=1位置4的分析:
子串: a b a b
前缀: a, ab, aba
后缀: b, ab, bab
相等前后缀: "ab" ← 所以next[4]=2
- 对于模式串
跳转的几何意义
匹配失败时:
主串: ... a b a b ? ...
模式串: a b a b c
↑ 失败位置j=4查看next[4]=2,意味着前4个字符"abab"中:
前缀"ab" = 后缀"ab"所以我们可以把模式串移动,让前缀对齐刚才匹配的后缀:
主串: ... a b a b ? ...
模式串: a b a b c
↑ 从位置2继续匹配
5.4 next数组的本质
是模式串的"自相似性"描述
记录每个位置前面子串的最长相等前后缀长度
指导匹配失败时的智能跳转
6. OJ例题及详解 (Java语言实现)
题目描述
学习KMP算法,给出主串和模式串,求模式串在主串的位置
算法框架参考课本第四章
输入
第一个输入t,表示有t个实例
第二行输入第1个实例的主串,第三行输入第1个实例的模式串
以此类推
输出
第一行输出第1个实例的模式串的next值
第二行输出第1个实例的匹配位置,位置从1开始计算,如果匹配成功输出位置,匹配失败输出0
以此类推
IO模式
本题IO模式为标准输入/输出(Standard IO),你需要从标准输入流中读入数据,并将答案输出至标准输出流中。
输入样例 3
qwertyuiop
tyu
aabbccdd
ccc
aaaabababac
abac 输出样例 -1 0 0
5
-1 0 1
0
-1 0 0 1
8
- 代码及详解
import java.util.Scanner;/*** KMP算法Java实现* 用于解决字符串匹配问题*/ public class Main {/*** 构建KMP算法的next数组* @param pattern 模式串* @return next数组* * next数组原理:* - next[j]表示当模式串中第j个字符与主串不匹配时,* 应该跳转到模式串的哪个位置继续比较* - next[0]固定为-1,表示模式串需要从头开始匹配*/public static int[] buildNext(String pattern) {int m = pattern.length();int[] next = new int[m];next[0] = -1; // 第一个字符的next值固定为-1int j = 0; // 主指针,遍历模式串int k = -1; // 前缀指针,记录最长公共前后缀长度// 构建next数组的核心循环while (j < m - 1) {// 情况1:k为-1,表示没有公共前后缀,从头开始// 情况2:当前字符匹配,找到更长的公共前后缀if (k == -1 || pattern.charAt(j) == pattern.charAt(k)) {j++;k++;next[j] = k; // 记录当前位置的next值} else {// 字符不匹配时,利用已计算的next值进行回溯k = next[k];}}return next;}/*** KMP字符串匹配算法* @param text 主串* @param pattern 模式串* @return 匹配位置(从1开始),未找到返回0* * 算法优势:* - 主串指针不回溯,提高匹配效率* - 时间复杂度O(n+m),其中n为主串长度,m为模式串长度*/public static int kmpSearch(String text, String pattern) {// 边界情况处理if (pattern.isEmpty()) return 1; // 空模式串默认匹配位置1if (pattern.length() > text.length()) return 0; // 模式串比主串长int n = text.length();int m = pattern.length();int[] next = buildNext(pattern);int i = 0; // 主串指针,永不回溯int j = 0; // 模式串指针// 匹配过程while (i < n && j < m) {// j == -1:模式串需要从头开始匹配// 当前字符匹配成功:继续比较下一个字符if (j == -1 || text.charAt(i) == pattern.charAt(j)) {i++;j++;} else {// 不匹配时,模式串指针根据next数组智能跳转j = next[j];}}// 判断匹配结果if (j == m) {// 匹配成功,返回位置(从1开始计算)return i - j + 1;} else {// 匹配失败return 0;}}/*** 主函数:处理输入输出*/public static void main(String[] args) {Scanner scanner = new Scanner(System.in);// 读取测试用例数量int t = scanner.nextInt();scanner.nextLine(); // 消耗换行符// 处理每个测试用例for (int i = 0; i < t; i++) {// 读取主串和模式串String text = scanner.nextLine();String pattern = scanner.nextLine();// 构建next数组int[] next = buildNext(pattern);// 输出next数组for (int j = 0; j < next.length; j++) {System.out.print(next[j] + " ");}System.out.println();// 执行KMP匹配并输出结果int pos = kmpSearch(text, pattern);System.out.println(pos);}scanner.close();} }