
在字符串处理场景中,字符串匹配是最基础也最常用的操作——比如文本编辑器的“查找”功能、日志检索、DNA序列比对等,核心需求都是“在一个长字符串(主串)中,快速找到一个短字符串(模式串)的位置”。
提到字符串匹配,很多人首先想到的是暴力匹配(BF算法),虽然逻辑简单、编码容易,但效率极低,在主串和模式串较长时会出现明显卡顿。而KMP算法作为字符串匹配的经典优化算法,凭借“避免重复比对”的核心思路,将时间复杂度从O(n*m)降至O(n+m),成为面试和实际开发中的高频考点与实用工具。
本文将从“暴力匹配的痛点”切入,一步步拆解KMP算法的核心思想、next数组的推导逻辑,再结合实战代码实现,让无论是新手还是有基础的开发者,都能彻底吃透KMP,看完就能上手使用。
在讲KMP之前,我们先简单回顾下暴力匹配(BF算法)的逻辑,明白其痛点,才能理解KMP的优化价值。
暴力匹配的核心逻辑:将主串的每一个字符作为起始位置,与模式串的第一个字符比对;如果相等,就继续比对下一个字符;如果不相等,主串的起始位置向后移1位,模式串重新回到第一个字符,从头开始比对,直到匹配成功或主串遍历完毕。
主串:ABABCABAA(长度n=9)
模式串:ABAA(长度m=4)
匹配过程中,当主串匹配到第5个字符(C)、模式串匹配到第4个字符(A)时,发现不相等。此时暴力匹配会让主串回到第2个字符(B),模式串回到第1个字符(A),重新开始比对——但这一步其实做了大量无用功:前面已经匹配成功的“AB”,其实可以直接复用,无需从头重来。
而KMP算法的核心优化,就是「匹配失败时,主串指针不回溯,只调整模式串指针的位置」,通过提前预处理模式串,找到“可复用的匹配片段”,跳过无用的重复比对,从而大幅提升效率。
KMP算法的核心逻辑可以概括为一句话:利用模式串自身的“前后缀重复规律”,提前生成一份“跳转指南”(next数组),匹配失败时,根据指南调整模式串指针,无需回溯主串,直接继续比对。
要搞懂“跳转指南”,首先要明确两个基础概念(以模式串为例,不包含整个串本身):
匹配失败时,模式串中已经匹配成功的部分,其“后缀”很可能与模式串开头的“前缀”重复——这部分重复的片段,无需重新比对,直接让模式串的前缀对齐主串的后缀,就能继续匹配。
比如前面的例子,模式串“ABAA”匹配到第4个字符(A)失败时,已经匹配成功的部分是“ABA”,其最长公共前后缀是“A”(长度1)。因此,模式串指针无需归零,直接跳到第2个字符(B),继续与主串当前位置(C)比对,跳过了“重新比对A”的无用功。
next数组是KMP算法的核心,它的长度与模式串长度一致,next[i]表示“模式串中第i个字符(下标从0开始)匹配失败时,模式串指针应该跳转的位置”,其本质就是“模式串前i个字符组成的子串的最长公共前后缀长度”。
最终得到next数组:[-1, 0, 0, 1],这就是模式串“ABAA”的跳转指南。
有了next数组,我们就可以按照以下流程完成主串与模式串的匹配,全程主串指针不回溯:
1. i=0,j=0:A==A,i=1,j=1;
2. i=1,j=1:B==B,i=2,j=2;
3. i=2,j=2:A==A,i=3,j=3;
4. i=3,j=3:B != A,j=next[3] = 1;
5. i=3,j=1:B==B,i=4,j=2;
6. i=4,j=2:C != A,j=next[2] = 0;
7. i=4,j=0:C != A,j=next[0] = -1;
8. j=-1,i=5,j=0:A==A,i=6,j=1;
9. i=6,j=1:B==B,i=7,j=2;
10. i=7,j=2:A==A,i=8,j=3;
11. i=8,j=3:A==A,i=9,j=4(j==m=4),匹配成功,返回位置9-4=5。
最终匹配结果:主串中从第5个位置(下标5)开始,存在与模式串“ABAA”完全匹配的子串,与预期一致。
下面给出KMP算法的完整C语言实现,包含next数组构建和字符串匹配两个核心函数,代码注释详细,可直接复制到项目中使用,适配大多数字符串匹配场景。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 构建模式串的next数组
void getNext(char* pattern, int* next, int m) {
int j = -1; // 指向最长公共前缀的末尾
next[0] = -1;
for (int i = 1; i < m; i++) {
// 回溯j,找到更短的公共前后缀
while (j != -1 && pattern[i] != pattern[j+1]) {
j = next[j];
}
// 匹配成功,更新j和next[i]
if (pattern[i] == pattern[j+1]) {
j++;
}
next[i] = j;
}
}
// KMP字符串匹配函数
// 主串s,模式串p,返回匹配开始的下标(-1表示匹配失败)
int kmpMatch(char* s, char* p) {
int n = strlen(s); // 主串长度
int m = strlen(p); // 模式串长度
if (m == 0) return 0; // 模式串为空,直接匹配成功
// 申请next数组,长度与模式串一致
int* next = (int*)malloc(sizeof(int) * m);
getNext(p, next, m); // 构建next数组
int j = -1; // 模式串指针
for (int i = 0; i < n; i++) {
// 匹配失败,调整模式串指针
while (j != -1 && s[i] != p[j+1]) {
j = next[j];
}
// 匹配成功,移动模式串指针
if (s[i] == p[j+1]) {
j++;
}
// 模式串全部匹配成功,返回起始下标
if (j == m - 1) {
free(next); // 释放内存,避免泄漏
return i - m + 1;
}
}
free(next); // 释放内存
return -1; // 匹配失败
}
// 测试示例
int main() {
char s[] = "ABABCABAA"; // 主串
char p[] = "ABAA"; // 模式串
int index = kmpMatch(s, p);
if (index != -1) {
printf("匹配成功,起始下标:%d\n", index);
} else {
printf("匹配失败\n");
}
return 0;
}上述实现的next数组,在模式串存在大量重复字符时,可能会出现不必要的跳转。比如模式串“AAAAA”,其next数组为[-1,0,1,2,3],当匹配失败时,会多次跳转但其实都是重复的A,效率仍有优化空间。
优化方案:构建nextval数组,在next数组的基础上,增加一步判断——如果模式串当前位置的字符与跳转位置的字符相同,则继续回溯,直到找到不同的字符或j=-1,避免重复跳转。
nextval数组的构建代码(替换getNext函数即可):
void getNextVal(char* pattern, int* nextval, int m) {
int j = -1;
nextval[0] = -1;
for (int i = 1; i < m; i++) {
while (j != -1 && pattern[i] != pattern[j+1]) {
j = nextval[j];
}
if (pattern[i] == pattern[j+1]) {
j++;
}
// 核心优化:判断当前字符与跳转位置字符是否相同
if (j == -1 || pattern[i+1] != pattern[j+1]) {
nextval[i] = j;
} else {
nextval[i] = nextval[j];
}
}
}优化后的nextval数组,能进一步减少跳转次数,在模式串重复字符较多的场景下,效率提升更明显。
KMP算法的核心价值,在于“利用模式串的前后缀重复规律,避免主串回溯,减少无用比对”,其时间复杂度优化到O(n+m),是字符串匹配领域的经典算法。
掌握KMP的关键,不在于死记硬背next数组的推导公式,而在于理解“最长公共前后缀”的意义——它本质是找到“可复用的匹配片段”,从而实现模式串的聪明跳转。本文从原理拆解到代码实现,一步步简化复杂概念,希望能帮助大家真正吃透KMP,无论是应对面试,还是实际开发中的字符串匹配需求,都能轻松应对。
最后,欢迎大家在评论区交流:你在使用KMP时遇到过哪些问题?有哪些优化技巧?一起探讨,共同进步!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。