1. 为什么我们需要理解KMP算法第一次接触KMP算法时很多人都会感到困惑。这个算法看起来简单但理解起来却不容易。传统的暴力匹配算法虽然直观但在处理长文本时效率低下。KMP算法通过预处理模式串构建next和nextval数组将匹配时间复杂度从O(m*n)降低到O(mn)。这种效率提升在处理大规模文本时尤为明显。我在学习KMP算法时最大的困扰就是next数组的计算。很多教材直接给出公式却没有解释背后的原理。直到我发现了最长公共前后缀这个概念一切才变得清晰起来。这个概念就像一把钥匙打开了理解KMP算法的大门。2. 最长公共前后缀理解KMP的核心2.1 什么是公共前后缀想象一下你有一串珠子a-b-a-b。这串珠子的前缀是指从开头开始的子串比如a、ab、aba等后缀则是指以结尾为终点的子串如b、ab、bab等。公共前后缀就是既是前缀又是后缀的子串。对于abab这个字符串长度为1前缀a后缀b → 无公共长度为2前缀ab后缀ab → 公共ab长度为3前缀aba后缀bab → 无公共所以abab的最长公共前后缀是ab长度为2。2.2 从公共前后缀到next数组next数组的核心思想就是利用模式串自身的结构信息。当匹配失败时我们不需要从头开始比较而是可以利用已经匹配的部分找到可以跳过比较的位置。计算next[j]的步骤找到子串P[1...j-1]的最长公共前后缀长度knext[j] k 1以字符串ababaaababaa为例next[1] 0约定next[2]子串a → 无公共 → next[2]1next[3]子串ab → 无公共 → next[3]1next[4]子串aba → 公共a → next[4]2next[5]子串abab → 公共ab → next[5]33. 手动计算next数组的详细步骤3.1 分步图解计算过程让我们以字符串ababaaababaa为例详细计算其next数组。初始化next[1] 0j2子串a前缀[a]后缀[a]最长公共前后缀a长度1next[2] 1 1 2但通常约定next[1]0, next[2]1这里有个细节需要注意有些教材定义next[j]为最长公共前后缀长度有些定义为长度1。在实际应用中要明确约定。j3子串ab前缀[a, ab]后缀[b, ab]公共前后缀ab但要求真前缀和真后缀即不能是整个字符串实际无公共 → next[3]1j4子串aba前缀[a, ab, aba]后缀[a, ba, aba]公共前后缀a长度1next[4]1123.2 常见错误与注意事项在计算next数组时有几个容易出错的地方忘记排除整个字符串本身作为前后缀的情况混淆了不同教材对next数组的定义长度 vs 长度1忽略了字符串下标从0开始还是从1开始的区别我建议在计算时明确字符串的起始下标本文示例从1开始使用表格记录每一步的结果对于每个j画出具体的前后缀对比图4. 从next到nextvalKMP的优化之路4.1 next数组的局限性虽然next数组已经大大提高了匹配效率但在某些情况下仍有优化空间。考虑模式串aaaaabnext数组[0,1,2,3,4,5]当在j5匹配失败时根据next会回退到j4仍然是a必然再次失败这种连续相同字符导致的多次无效回退正是nextval要解决的问题。4.2 nextval数组的计算方法nextval的计算基于next数组规则如下nextval[1] 0对于j1如果P[j] ≠ P[next[j]]则nextval[j] next[j]如果P[j] P[next[j]]则nextval[j] nextval[next[j]]让我们继续用ababaaababaa的例子已知next数组[0,1,1,2,3,4,2,2,3,4,5,6]计算nextvalnextval[1] 0j2P[2]b, P[next[2]]P[1]a → 不等 → nextval[2]next[2]1j3P[3]a, P[next[3]]P[1]a → 相等 → nextval[3]nextval[next[3]]nextval[1]0j4P[4]b, P[next[4]]P[2]b → 相等 → nextval[4]nextval[next[4]]nextval[2]1j5P[5]a, P[next[5]]P[3]a → 相等 → nextval[5]nextval[next[5]]nextval[3]04.3 nextval的优势与应用场景nextval数组通过避免不必要的回退进一步提升了匹配效率。特别是在模式串中有大量重复字符时优化效果更为明显。在实际应用中对于随机性较强的模式串next和nextval性能差异不大对于有大量重复子串的模式串nextval可以显著减少比较次数在实现时可以先计算next数组再推导出nextval数组5. 实战演练完整计算示例让我们通过一个完整的例子巩固所学内容。考虑模式串aabaaab5.1 计算next数组初始化next[1] 0j2子串a公共前后缀无next[2] 1j3子串aa公共前后缀a长度1next[3] 1 1 2j4子串aab公共前后缀无next[4] 1j5子串aaba公共前后缀a长度1next[5] 2j6子串aabaa公共前后缀aa长度2next[6] 3j7子串aabaaa公共前后缀aa长度2next[7] 3next数组[0,1,2,1,2,3,3]5.2 计算nextval数组nextval[1] 0j2P[2]a, P[next[2]]P[1]a → 相等 → nextval[2]nextval[next[2]]nextval[1]0j3P[3]b, P[next[3]]P[2]a → 不等 → nextval[3]next[3]2j4P[4]a, P[next[4]]P[1]a → 相等 → nextval[4]nextval[next[4]]nextval[1]0j5P[5]a, P[next[5]]P[2]a → 相等 → nextval[5]nextval[next[5]]nextval[2]0j6P[6]a, P[next[6]]P[3]b → 不等 → nextval[6]next[6]3j7P[7]b, P[next[7]]P[3]b → 相等 → nextval[7]nextval[next[7]]nextval[3]2nextval数组[0,0,2,0,0,3,2]6. 常见问题与解决技巧在教学过程中我发现学生们经常遇到以下几个问题混淆前后缀的概念前缀必须从第一个字符开始后缀必须以最后一个字符结束公共前后缀不能是整个子串本身计算nextval时忘记递归当P[j] P[next[j]]时需要递归查找nextval[next[j]]这是一个容易忽略的细节下标问题明确字符串是从0开始还是从1开始不同教材可能有不同的约定在实际编程中从0开始更为常见我的建议是对于每个j画出具体的子串和前后缀使用表格记录中间结果多做练习熟能生巧7. 从理论到实践代码实现要点理解了手动计算的方法后我们来看看如何用代码实现next和nextval数组的生成。以下是Python实现的伪代码def compute_next(pattern): next_arr [0] * (len(pattern) 1) # 假设下标从1开始 next_arr[1] 0 i, j 1, 0 while i len(pattern): if j 0 or pattern[i-1] pattern[j-1]: i 1 j 1 next_arr[i] j else: j next_arr[j] return next_arr def compute_nextval(pattern, next_arr): nextval [0] * (len(next_arr)) nextval[1] 0 for j in range(2, len(next_arr)): if pattern[j-1] pattern[next_arr[j]-1]: nextval[j] nextval[next_arr[j]] else: nextval[j] next_arr[j] return nextval实现时需要注意字符串下标的处理编程语言通常从0开始边界条件的检查递归查找nextval时的效率问题在实际项目中我建议对next和nextval的计算进行单元测试对于特别长的模式串考虑优化算法可以将预处理结果缓存起来重复使用
告别死记硬背:用“最长公共前后缀”图解KMP的next与nextval数组
发布时间:2026/7/18 0:50:59
1. 为什么我们需要理解KMP算法第一次接触KMP算法时很多人都会感到困惑。这个算法看起来简单但理解起来却不容易。传统的暴力匹配算法虽然直观但在处理长文本时效率低下。KMP算法通过预处理模式串构建next和nextval数组将匹配时间复杂度从O(m*n)降低到O(mn)。这种效率提升在处理大规模文本时尤为明显。我在学习KMP算法时最大的困扰就是next数组的计算。很多教材直接给出公式却没有解释背后的原理。直到我发现了最长公共前后缀这个概念一切才变得清晰起来。这个概念就像一把钥匙打开了理解KMP算法的大门。2. 最长公共前后缀理解KMP的核心2.1 什么是公共前后缀想象一下你有一串珠子a-b-a-b。这串珠子的前缀是指从开头开始的子串比如a、ab、aba等后缀则是指以结尾为终点的子串如b、ab、bab等。公共前后缀就是既是前缀又是后缀的子串。对于abab这个字符串长度为1前缀a后缀b → 无公共长度为2前缀ab后缀ab → 公共ab长度为3前缀aba后缀bab → 无公共所以abab的最长公共前后缀是ab长度为2。2.2 从公共前后缀到next数组next数组的核心思想就是利用模式串自身的结构信息。当匹配失败时我们不需要从头开始比较而是可以利用已经匹配的部分找到可以跳过比较的位置。计算next[j]的步骤找到子串P[1...j-1]的最长公共前后缀长度knext[j] k 1以字符串ababaaababaa为例next[1] 0约定next[2]子串a → 无公共 → next[2]1next[3]子串ab → 无公共 → next[3]1next[4]子串aba → 公共a → next[4]2next[5]子串abab → 公共ab → next[5]33. 手动计算next数组的详细步骤3.1 分步图解计算过程让我们以字符串ababaaababaa为例详细计算其next数组。初始化next[1] 0j2子串a前缀[a]后缀[a]最长公共前后缀a长度1next[2] 1 1 2但通常约定next[1]0, next[2]1这里有个细节需要注意有些教材定义next[j]为最长公共前后缀长度有些定义为长度1。在实际应用中要明确约定。j3子串ab前缀[a, ab]后缀[b, ab]公共前后缀ab但要求真前缀和真后缀即不能是整个字符串实际无公共 → next[3]1j4子串aba前缀[a, ab, aba]后缀[a, ba, aba]公共前后缀a长度1next[4]1123.2 常见错误与注意事项在计算next数组时有几个容易出错的地方忘记排除整个字符串本身作为前后缀的情况混淆了不同教材对next数组的定义长度 vs 长度1忽略了字符串下标从0开始还是从1开始的区别我建议在计算时明确字符串的起始下标本文示例从1开始使用表格记录每一步的结果对于每个j画出具体的前后缀对比图4. 从next到nextvalKMP的优化之路4.1 next数组的局限性虽然next数组已经大大提高了匹配效率但在某些情况下仍有优化空间。考虑模式串aaaaabnext数组[0,1,2,3,4,5]当在j5匹配失败时根据next会回退到j4仍然是a必然再次失败这种连续相同字符导致的多次无效回退正是nextval要解决的问题。4.2 nextval数组的计算方法nextval的计算基于next数组规则如下nextval[1] 0对于j1如果P[j] ≠ P[next[j]]则nextval[j] next[j]如果P[j] P[next[j]]则nextval[j] nextval[next[j]]让我们继续用ababaaababaa的例子已知next数组[0,1,1,2,3,4,2,2,3,4,5,6]计算nextvalnextval[1] 0j2P[2]b, P[next[2]]P[1]a → 不等 → nextval[2]next[2]1j3P[3]a, P[next[3]]P[1]a → 相等 → nextval[3]nextval[next[3]]nextval[1]0j4P[4]b, P[next[4]]P[2]b → 相等 → nextval[4]nextval[next[4]]nextval[2]1j5P[5]a, P[next[5]]P[3]a → 相等 → nextval[5]nextval[next[5]]nextval[3]04.3 nextval的优势与应用场景nextval数组通过避免不必要的回退进一步提升了匹配效率。特别是在模式串中有大量重复字符时优化效果更为明显。在实际应用中对于随机性较强的模式串next和nextval性能差异不大对于有大量重复子串的模式串nextval可以显著减少比较次数在实现时可以先计算next数组再推导出nextval数组5. 实战演练完整计算示例让我们通过一个完整的例子巩固所学内容。考虑模式串aabaaab5.1 计算next数组初始化next[1] 0j2子串a公共前后缀无next[2] 1j3子串aa公共前后缀a长度1next[3] 1 1 2j4子串aab公共前后缀无next[4] 1j5子串aaba公共前后缀a长度1next[5] 2j6子串aabaa公共前后缀aa长度2next[6] 3j7子串aabaaa公共前后缀aa长度2next[7] 3next数组[0,1,2,1,2,3,3]5.2 计算nextval数组nextval[1] 0j2P[2]a, P[next[2]]P[1]a → 相等 → nextval[2]nextval[next[2]]nextval[1]0j3P[3]b, P[next[3]]P[2]a → 不等 → nextval[3]next[3]2j4P[4]a, P[next[4]]P[1]a → 相等 → nextval[4]nextval[next[4]]nextval[1]0j5P[5]a, P[next[5]]P[2]a → 相等 → nextval[5]nextval[next[5]]nextval[2]0j6P[6]a, P[next[6]]P[3]b → 不等 → nextval[6]next[6]3j7P[7]b, P[next[7]]P[3]b → 相等 → nextval[7]nextval[next[7]]nextval[3]2nextval数组[0,0,2,0,0,3,2]6. 常见问题与解决技巧在教学过程中我发现学生们经常遇到以下几个问题混淆前后缀的概念前缀必须从第一个字符开始后缀必须以最后一个字符结束公共前后缀不能是整个子串本身计算nextval时忘记递归当P[j] P[next[j]]时需要递归查找nextval[next[j]]这是一个容易忽略的细节下标问题明确字符串是从0开始还是从1开始不同教材可能有不同的约定在实际编程中从0开始更为常见我的建议是对于每个j画出具体的子串和前后缀使用表格记录中间结果多做练习熟能生巧7. 从理论到实践代码实现要点理解了手动计算的方法后我们来看看如何用代码实现next和nextval数组的生成。以下是Python实现的伪代码def compute_next(pattern): next_arr [0] * (len(pattern) 1) # 假设下标从1开始 next_arr[1] 0 i, j 1, 0 while i len(pattern): if j 0 or pattern[i-1] pattern[j-1]: i 1 j 1 next_arr[i] j else: j next_arr[j] return next_arr def compute_nextval(pattern, next_arr): nextval [0] * (len(next_arr)) nextval[1] 0 for j in range(2, len(next_arr)): if pattern[j-1] pattern[next_arr[j]-1]: nextval[j] nextval[next_arr[j]] else: nextval[j] next_arr[j] return nextval实现时需要注意字符串下标的处理编程语言通常从0开始边界条件的检查递归查找nextval时的效率问题在实际项目中我建议对next和nextval的计算进行单元测试对于特别长的模式串考虑优化算法可以将预处理结果缓存起来重复使用