KMP 算法详解
KMP 算法(Knuth-Morris-Pratt 算法)是一种高效的字符串匹配算法,它可以在 O(n+m) 时间复杂度内完成模式串在主串中的搜索工作,其中 n 是主串的长度,m 是模式串的长度。KMP 算法的核心思想是利用模式串自身的结构特点,避免不必要的回溯,从而提高效率。
本文档将详细介绍 KMP 算法的原理、步骤以及如何在 C 语言中实现。
1. KMP 算法原理
1.1 前缀与后缀
对于模式串 s
,一个长度为 k
的前缀是指从模式串开头连续取 k
个字符形成的子串;一个长度为 k
的后缀是指从模式串末尾连续取 k
个字符形成的子串。
1.2 最长公共前后缀
最长公共前后缀是指模式串中最长的一个既是前缀又是后缀的子串。例如,对于模式串 ABCDABCD
,最长公共前后缀是 ABCD
。
1.3 next 数组
next 数组是 KMP 算法中的关键数据结构,它记录了模式串中每个位置的最长公共前后缀的长度。对于模式串 s
,next[j]
表示 s[1..j]
的最长公共前后缀的长度。
1.4 nextval 数组
nextval 数组是 next 数组的一种优化版本,它可以更高效地跳过不必要的比较。nextval[j]
表示 s[1..j]
的最长公共前后缀的长度,但当 s[next[j]] != s[j]
时,可以直接跳到 nextval[j]
。
2. KMP 算法步骤
2.1 获取 next 数组 (getNext
)
此函数用于计算模式串的 next 数组。
//获得next数组,s为模式串,next为所求next数组
void getNext(char *s,int *next){
//i表示从第二个字符开始,
int i = 1;
//k表示最长公共前后缀的数量
int k = 0;
//next[0]没有值,next[1]为第一个字符,没有前后缀,直接赋值为0
next[1] = 0;
//模式串的长度,这里之所以要减一,是因为把\0也算在里面了
int length = strlen(s)-1;
//从第二个字符开始,一直到最后一个字符
while(i < length){
//1.j等于0,说明现在是第二个字符,前面只有一个字符,则赋值为1
//2.当s[i]==s[k],s[i]表示现在扫描到的字符,与前一个公共最长前后缀的前缀的后一个字符比较
//如果相等,说明该公共最长前后缀加1
//如果不相等,就让k等于前一个公共最长前后缀的前缀的后一个字符比较,之后再次循环判断
if(k == 0 || s[i] == s[k]){
i++;
k++;
next[i] = k;
} else {
k = next[k];
}
}
printf("下面是next数组数据\n");
for(int j = 0; j <= length; j++){
printf("%d ",next[j]);
}
printf("\n");
}
参数:
s
: 模式串。next
: next 数组。
功能:
- 初始化
next[1]
为 0。 - 通过比较模式串中的字符,填充
next
数组。
2.2 获取 nextval 数组 (getNextVal
)
此函数用于计算模式串的 nextval 数组。
void getNextVal(char *s,int *nextval,int *next){
nextval[1] = 0;
for(int j=2;j<=strlen(s)-1;j++){
if(s[next[j]]==s[j]){
nextval[j]=nextval[next[j]];
} else {
nextval[j]=next[j];
}
}
printf("下面是nextval数组数据\n");
for(int j = 0; j <= strlen(s)-1; j++){
printf("%d ",nextval[j]);
}
printf("\n");
}
参数:
s
: 模式串。nextval
: nextval 数组。next
: 已经计算好的 next 数组。
功能:
- 初始化
nextval[1]
为 0。 - 根据 next 数组计算 nextval 数组。
2.3 KMP 方法 (KMP
)
此函数用于在主串中查找模式串。
//KMP方法,返回第一个匹配模式串的开始下标
int KMP(char *s,char *modelStr){
//i表示查找字符串的下标
int i = 1;
//j表示模式串的下标
int j = 1;
//模式串的长度 ,这里之所以要减一,是因为把\0也算在里面了
int length = strlen(modelStr)-1;
printf("模式串的长度:%d\n",length);
//声明next数组
int next[length+1];
//声明nextval数组
int nextval[length+1];
//获取next数组
getNext(modelStr,next);
//获得更优的nextval数组
getNextVal(modelStr,nextval,next);
while(i <= strlen(s)-1 && j <= strlen(modelStr)-1){
//当第1个元素匹配失败时,匹配下一个相邻子串,令j=0,i++,j++
//如果匹配成功再往后匹配一个字符,直到结束
//如果匹配不成功,那么就让j移动到next[j],再进行比较
if(j == 0 || s[i] == modelStr[j]){
i++;
j++;
} else {
j = nextval[j];
}
}
if(j > length){
return i - length;
} else {
return -1;
}
}
参数:
s
: 主串。modelStr
: 模式串。
返回值:
- 如果找到模式串,则返回模式串在主串中的起始位置(从 1 开始计数)。
- 如果未找到模式串,则返回 -1。
功能:
- 调用
getNext
和getNextVal
函数计算 next 和 nextval 数组。 - 在主串中进行模式串的匹配,使用 nextval 数组跳过不必要的比较。
3. 示例程序
下面是一个示例程序,演示了如何使用上述定义的功能。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//优化的KMP算法
//获得next数组,s为模式串,next为所求next数组
void getNext(char *s,int *next){
//i表示从第二个字符开始,
int i = 1;
//k表示最长公共前后缀的数量
int k = 0;
//next[0]没有值,next[1]为第一个字符,没有前后缀,直接赋值为0
next[1] = 0;
//模式串的长度,这里之所以要减一,是因为把\0也算在里面了
int length = strlen(s)-1;
//从第二个字符开始,一直到最后一个字符
while(i < length){
//1.j等于0,说明现在是第二个字符,前面只有一个字符,则赋值为1
//2.当s[i]==s[k],s[i]表示现在扫描到的字符,与前一个公共最长前后缀的前缀的后一个字符比较
//如果相等,说明该公共最长前后缀加1
//如果不相等,就让k等于前一个公共最长前后缀的前缀的后一个字符比较,之后再次循环判断
if(k == 0 || s[i] == s[k]){
i++;
k++;
next[i] = k;
} else {
k = next[k];
}
}
printf("下面是next数组数据\n");
for(int j = 0; j <= length; j++){
printf("%d ",next[j]);
}
printf("\n");
}
void getNextVal(char *s,int *nextval,int *next){
nextval[1] = 0;
for(int j=2;j<=strlen(s)-1;j++){
if(s[next[j]]==s[j]){
nextval[j]=nextval[next[j]];
} else {
nextval[j]=next[j];
}
}
printf("下面是nextval数组数据\n");
for(int j = 0; j <= strlen(s)-1; j++){
printf("%d ",nextval[j]);
}
printf("\n");
}
//KMP方法,返回第一个匹配模式串的开始下标
int KMP(char *s,char *modelStr){
//i表示查找字符串的下标
int i = 1;
//j表示模式串的下标
int j = 1;
//模式串的长度 ,这里之所以要减一,是因为把\0也算在里面了
int length = strlen(modelStr)-1;
printf("模式串的长度:%d\n",length);
//声明next数组
int next[length+1];
//声明nextval数组
int nextval[length+1];
//获取next数组
getNext(modelStr,next);
//获得更优的nextval数组
getNextVal(modelStr,nextval,next);
while(i <= strlen(s)-1 && j <= strlen(modelStr)-1){
//当第1个元素匹配失败时,匹配下一个相邻子串,令j=0,i++,j++
//如果匹配成功再往后匹配一个字符,直到结束
//如果匹配不成功,那么就让j移动到next[j],再进行比较
if(j == 0 || s[i] == modelStr[j]){
i++;
j++;
} else {
j = nextval[j];
}
}
if(j > length){
return i - length;
} else {
return -1;
}
}
int main() {
char s[1001]; // 主字符串
char modelStr[1001]; // 模式串
printf("请输入主字符串: ");
scanf("%s", &s[1]);
printf("请输入要查找的模式串: ");
scanf("%s", &modelStr[1]);
printf("主字符串:");
for(int i = 1; i < strlen(s); i++){
printf("%c",s[i]);
}
printf("\n");
printf("模式串:");
for(int i = 1; i < strlen(modelStr); i++){
printf("%c",modelStr[i]);
}
printf("\n");
int pos = KMP(s, modelStr);
if (pos != -1) {
printf("模式串在主字符串中的起始位置是: %d\n", pos); // 输出匹配的位置,注意下标从1开始
} else {
printf("模式串在主字符串中未找到。\n");
}
return 0;
}
4. 使用说明
- 编译并运行上述示例程序。
- 根据提示输入主串和模式串。
- 观察并理解 KMP 算法的输出结果。
5. 总结
通过本文档,您可以了解到 KMP 算法的基本概念和实现方法。使用这些知识,您可以轻松地处理字符串匹配问题,特别是在大数据量的情况下,KMP 算法能显著提升性能。希望这份指南能够帮助您更好地理解和使用 KMP 算法。