手写KMP的核心价值是“可控”而非“更快”,支持多次复用模式串、获取所有匹配位置、流式增量匹配,并可定制失配逻辑、忽略大小写等;其关键步骤为构建next数组、主串遍历、失配查表跳转。

为什么不用 strings.Index 而要手写 KMP?
因为 strings.Index 底层确实是 KMP(Go 1.19+ 在多数场景已切到 KMP 或优化版 BM),但如果你需要:多次复用同一模式串、获取所有匹配位置、或在流式数据中增量匹配,标准库就不够用了。手写 KMP 的核心价值不是“更快”,而是“可控”——比如跳过已知前缀、定制失配回退逻辑、或嵌入自定义比较(如忽略大小写但不提前转全小写)。
手动实现 KMP 的三个关键步骤
KMP 不是黑盒,它就靠三步撑起来:构建 next 数组、主串遍历、失配时查表跳转。容易错的点不在循环逻辑,而在 next 的构造边界。
-
构建
next数组时,下标从 0 开始,next[0] = 0是定死的,别设成 -1;否则后续匹配时j = next[j-1]会越界 - 匹配循环里,
i只进不退,j在失配时才回退:if j > 0 && pattern[j] != text[i] { j = next[j-1] }—— 注意是j-1查表,不是j - 找到匹配后,如果你想继续找下一个,别直接
i++, j++,而要j = next[j](利用已匹配后缀),否则漏掉重叠匹配,比如在"aaaa"中找"aa",应返回位置 0、1、2,不是只 0、2
Go 里 next 数组怎么写才不踩内存坑?
Go 切片底层数组可能被复用,如果 next 是局部切片且长度不定,建议显式初始化:next := make([]int, len(pattern))。别用 append 动态扩,否则 next[i] 可能读到旧值(尤其测试多个 pattern 时)。另外,next 只依赖 pattern,可预计算缓存,比如封装成 type KMP struct { pattern string; next []int },避免每次匹配都重建。
简单示例(非完整结构体):
立即学习“go语言免费学习笔记(深入)”;
func buildNext(pattern string) []int {
next := make([]int, len(pattern))
for i, j := 1, 0; i < len(pattern); i++ {
for j > 0 && pattern[i] != pattern[j] {
j = next[j-1]
}
if pattern[i] == pattern[j] {
j++
}
next[i] = j
}
return next
}匹配结果位置和标准库行为对齐吗?
Go 标准库返回的是第一个匹配的起始索引(int),没找到返回 -1。你手写的也该保持一致。但注意:KMP 自然支持重叠匹配,而 strings.Index 不重叠(找到一个就从下一个字符继续搜)。如果你要兼容标准库语义,匹配成功后得把 i 设为 i - j + 1 再加 1,跳过整个已匹配段;如果要重叠,则 i 只加 1,j 设为 next[j]。这个分支选错,结果就差一倍。
真正麻烦的不是算法本身,是搞清你要的「匹配语义」——是找所有出现,还是只找第一个;是否允许重叠;要不要支持 rune 级别(此时不能直接用 pattern[i],得转 []rune 并同步维护 next 的 rune 偏移)。这些决定写在最前面,比调 bug 省三小时。










