- gf24153 的博客
《Mod笔谈:KMP前传之公共前后缀》
- @ 2026-9-28 11:34:03
公共前后缀:KMP算法的“摸鱼神器”
当你的字符匹配主串和模式串匹配到第100个字符时才发现这个老牧师的***(出题人肯定干过这种事)写了个sz代码,按照暴力匹配的老规矩,主串指针得乖乖退回到第2个字符,模式串从头开始重新比对,相当于前面99个字符的匹配全白干,纯纯无效加班。公共前后缀就是专门来拯救这场“无效返工”的核心逻辑,堪称字符串匹配界的摸鱼小天才。
1. 概念拆解:什么是公共前后缀?
先给它拆明白:
- 前缀:就是一个字符串掐掉最后一个字符,从开头能拆出来的所有连续子串。比如“花生米”的前缀就是“花”、“花生”,绝对不能把完整的“花生米”算进去。
- 后缀:反过来,是掐掉第一个字符,从末尾拆出来的所有连续子串。对应“花生米”的后缀就是“米”、“生米”,同样不能包含完整字符串。
- 公共前后缀:就是前缀和后缀里“撞号”的那部分。
举个栗子,“花生米”里找不出重合的前缀后缀,它的最长公共前后缀长度就是 0,属于完全没有“复用价值”的字符串。但如果是“ababa”,它的前缀有 a, ab, aba, abab,后缀有 a, ba, aba, baba,其中 a 和 aba 是公共的,最长的就是 aba,长度为3。
2. 核心原理:为什么它能?
别小看这个平平无奇的概念,它就是 KMP算法 能让主串指针“绝不后退”的核心底气。
当匹配半路翻车时,我们根本不用把之前比对过的字符重新跑一遍——因为前面已经匹配成功的那一大段字符串里,藏着一段完全重合的公共前后缀。这部分内容本来就和主串里的对应字符对上了,直接把模式串指针跳到这段公共前后缀的末尾下一位,就能接着往下比对。
这相当于直接跳过了几百个没必要重复做的步骤,把原本可能 的慢动作,拉成了线性级别的丝滑速度。
3. 代码实现逻辑:自己跟自己对“暗号”
放到代码原理里,我们根本不用每次都从头遍历所有子串找公共前后缀,只需要用两个指针在模式串上走一遍就能搞定(通常称为构建 next 数组或 prefix 表):
-
双指针登场:
- 一个后缀指针(通常记为
i)往前扫,代表当前正在计算哪个位置的最长公共前后缀。 - 一个前缀指针(通常记为
j)专门记录当前最长公共前后缀的长度,同时也指向下一个待匹配的字符位置。
- 一个后缀指针(通常记为
-
匹配成功:
- 如果两个指针指向的字符刚好对上(
pattern[i] == pattern[j]),说明最长公共前后缀能再长一格。 - 操作:
j加 1,记录下当前位置i的最长公共前后缀长度为j,然后两个指针一起往前挪。
- 如果两个指针指向的字符刚好对上(
-
匹配失败:
- 要是字符对不上,
j不能直接归零,那样太浪费。j会往回跳,跳到上一个更短的公共前后缀的末尾位置(即j = next[j-1]),接着和当前后缀指针i对比。 - 这步回溯的逻辑是:既然长的公共前后缀对不上,那就看看短一点的有没有可能对上,充分利用已知的信息。
- 要是字符对不上,
-
彻底没戏:
- 直到前缀指针
j回到起点(j=0)还对不上,就说明当前位置没有公共前后缀,直接把长度记成0,然后后缀指针i继续往前走。
- 直到前缀指针
整个过程相当于模式串自己和自己先对了一遍“暗号”,提前把每一步匹配失败后该跳去的位置都记好。后面正式和主串匹配的时候,全程不做半分无用功,主打一个高效摸鱼。
注:以上内容为DeePseek生成
代码演示
void nxt() {
// 初始化:第一个字符没有前缀和后缀,所以最长公共前后缀长度为0
nt[0] = 0;
ans = 0; // 用于记录整个模式串中出现的最大公共前后缀长度(可选功能)
// j 代表“前缀末尾”的位置,同时也代表“当前已匹配的最长公共前后缀长度”
// 初始时,对于 i=1 的情况,我们假设前一个位置(i-1=0)的最长匹配长度是0
int j = 0;
// 标准写法通常在外层维护 j,或者像下面这样在循环内基于 nt[i-1] 获取初始 j。
// 遍历模式串的每一个位置 i,从第2个字符(下标1)开始,直到最后一个字符
for (int i = 1; i < n; i++) {
// 【关键步骤1】继承上一位的“遗产”
// nt[i-1] 记录了 s[0...i-1] 这个子串的最长公共前后缀长度。
// 这意味着 s[0...j-1] 和 s[i-j...i-1] 是完全一样的。
// 所以我们尝试接着这个长度往后比,看 s[i] 能不能和 s[j] 对上。
int j = nt[i - 1];
// 【关键步骤2】如果不匹配,就疯狂“回溯”
// 当 j > 0 说明还有更短的公共前后缀可以尝试;
// 当 s[i] != s[j] 说明当前长度的前缀后缀接不上了。
// 这时,我们不能直接清零,而是要跳到“更短一级的公共前后缀”去试试。
// nt[j-1] 就是 s[0...j-1] 这个前缀串本身的最长公共前后缀长度。
// 这步操作保证了我们不会漏掉任何可能的匹配机会,同时避免了暴力回退。
while (j > 0 && s[i] != s[j]) {
j = nt[j - 1];
}
// 【关键步骤3】如果匹配成功,长度加1
// 经过上面的回溯,要么 j=0(没得退了),要么 s[i]==s[j](对上了)。
// 如果对上了,说明在原来的公共前后缀基础上,又多匹配了一个字符。
if (s[i] == s[j]) {
j++;
}
// 【关键步骤4】记录结果
// 现在 j 的值,就是 s[0...i] 这个子串的最长公共前后缀长度。
nt[i] = j;
// 更新全局最大值(如果题目需要求整个串的最大对称程度)
ans = max(ans, j);
}
}