❤️博客主页: 小镇敲码人
欢迎关注:点赞 留言 收藏
努力和收获,都是自己的,与他人无关。你必须特别努力,才能显得毫不费力。坚信一句话:只有自己足够强大,才不会被别人践踏。
❤️人的能量=思想+行动速度的平方。
在【C语言进阶技巧】探秘字符与字符串函数的奇妙世界这篇文章中,我们模拟实现了字符串查找函数
strstr
,但我们实现的那个办法需要主串不停的回退,当发现有不匹配的字符时,子串也得重新回退到第一个字符的位置,这样是最简单的暴力求解,它还有一个名字叫做BF算法,并且它的时间复杂度已经达到了 O ( m ∗ n ) O(m*n) O(m∗n),今天我们来介绍一种时间复杂度只有 O ( m + n ) O(m+n) O(m+n)的字符串匹配算法,它就是KMP算法。
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个
next()
函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。-- 来自百度百科
可以发现第一次回退的下标是那两个相同子串的长度,由上述的阐述我们也可以知道,回退的位置与主串并没有关系,因为主串的那一段字符在子串中都是有对应的一段的,所以我们计算回退位置只看子串就行了,子串的每一个字符的位置都对应着一个回退的位置,它都可以用我们刚刚的方法求出,而储存这个回退位置的数组我们称之为next
数组。
KMP算法的精髓就在于 n e x t next next数组:也就是用 n e x t [ i ] = k next[i] = k next[i]=k来表示。这个 k k k就是当j
位置匹配不成功时,它要回退的第一个位置。
假设
j
位置匹配不成功,它的第一次回退规则是这样的:
- 如果在0位置就回退到-1下标处(特殊处理,便于后续的代码实现)
- 如果在其它位置,就看 0 到 j − 1 0到j-1 0到j−1位置是否存在最大长度的两个子字符串,他们必须满足这样的要求
- 前后缀相同。
- 第一个的子串前缀位置在 0 0 0下标处,第二个子串的后缀位置在 j − 1 j-1 j−1下标处。
- 回退的数组下标就等于这两个子字符串的长度。
next[0] = -1 next[1] = 0;
j
下标对应next数组的值如何求:手算next数组我们会算了,那再程序中又应该如何来处理呢?我们依旧画图来分析:
#include
#include
#include
#include
// KMP算法中的Getnext函数,用于生成子串Sub的next数组
void Getnext(int* next, char* Sub, int lenSub)
{
// 特殊情况下,当子串长度小于2时,直接设置next[0]为-1
if (lenSub < 2)
next[0] = -1;
else
{
// 初始化next数组的前两个元素
next[0] = -1;
next[1] = 0;
int k = 0; // k表示最大相同前后缀的长度
int i = 2; // i表示当前需要计算next值的下标
// 使用循环计算next数组的值
while (i < lenSub)
{
// 若k等于-1,或者Sub[k]等于Sub[i-1],则next[i]的值为k+1,然后i和k都自增1
if (k == -1 || Sub[k] == Sub[i - 1])
{
next[i] = k + 1;
i++;
k++;
}
// 若Sub[k]不等于Sub[i-1],则将k的值更新为next[k],继续检查
else
{
k = next[k];
}
}
}
}
// KMP算法实现,用于在主串str中查找子串Sub,返回第一次出现的位置
char* KMP(char* str, char* Sub, int pos)
{
assert(str && Sub); // 断言str和Sub非空
int lenstr = strlen(str);
int lenSub = strlen(Sub);
// 特殊情况下,当主串或子串为空串时,直接返回NULL
if (str[0] == '\0' || Sub[0] == '\0')
return NULL;
// 特殊情况下,当查找位置小于0或超出主串长度减1时,返回NULL
if (pos < 0 || pos > lenstr - 1)
return NULL;
int* next = (int*)malloc(sizeof(int) * lenSub); // 分配next数组的内存空间
assert(next); // 确保内存分配成功
Getnext(next, Sub, lenSub); // 调用Getnext函数生成子串Sub的next数组
int i = pos; // i表示在主串中的当前查找位置
int j = 0; // j表示在子串中的当前查找位置
// 使用循环进行匹配
while (i < lenstr && j < lenSub)
{
// 若j等于-1,或者主串当前字符和子串当前字符相等,则i和j分别自增1,继续匹配
if (j == -1 || str[i] == Sub[j])
{
i++;
j++;
}
// 若主串当前字符和子串当前字符不相等,则将j的值更新为next[j],继续匹配
else
{
j = next[j];
}
}
free(next); // 释放next数组的内存空间
next = NULL; // 将next指针置为NULL,防止产生野指针
// 若j等于子串长度,说明匹配成功,返回匹配到的位置的指针
if (j >= lenSub)
{
return str + (i - j);
}
// 若匹配失败,返回NULL
return NULL;
}
int main()
{
char str1[] = "abcdefabcdef";
char Sub[] = "a";
char* ret = KMP(str1, Sub, 0); // 在主串str1中查找子串Sub,从位置0开始查找
if (ret != NULL)
{
printf("%s\n", ret);
}
else
{
printf("子串未在主串中找到\n");
}
return 0;
}
注意:这里我们的i是从i = 2开始计算next数组的,因为前两个值已经已知了,后续求next数组,就相当于已知next[i-1]求next[i],此时判断等于的条件应该变成Sub[k] == Sub[i-1]
,这是和我们分析时有所区别的。
另外匹配成功时,返回的地址是主串中匹配成功那段子串的起始地址
我们画图来分析为什么是str+i-j
:
运行结果:
找到了就和strstr
函数一样从找到位置开始打印主串。
这里我们依旧通过画图来分析:
#include
#include
#include
#include
// 改进后的KMP算法中的Getnext函数,用于生成子串Sub的next数组
void Getnext(int* next, char* Sub, int lenSub)
{
// 特殊情况下,当子串长度小于2时,直接设置next[0]为-1
if (lenSub < 2)
next[0] = -1;
else
{
// 初始化next数组的前两个元素
next[0] = -1;
next[1] = 0;
int k = 0; // k表示最大相同前后缀的长度
int i = 2; // i表示当前需要计算next值的下标
// 使用循环计算next数组的值
while (i < lenSub)
{
// 若k等于-1,或者Sub[k]等于Sub[i-1],则next[i]的值为k+1,然后i和k都自增1
if (k == -1 || Sub[k] == Sub[i - 1])
{
next[i] = k + 1;
i++;
k++;
}
// 若Sub[k]不等于Sub[i-1],则将k的值更新为next[k],继续检查
else
{
k = next[k];
}
}
i = 1;
// 进行第二次优化,处理next数组中连续相等的情况
while (i < lenSub)
{
if (Sub[i] == Sub[next[i]])
{
next[i] = next[next[i]];
}
i++;
}
}
}
// 改进后的KMP算法实现,用于在主串str中查找子串Sub,返回第一次出现的位置
char* KMP(char* str, char* Sub, int pos)
{
assert(str && Sub); // 断言str和Sub非空
int lenstr = strlen(str);
int lenSub = strlen(Sub);
// 特殊情况下,当主串或子串为空串时,直接返回NULL
if (str[0] == '\0' || Sub[0] == '\0')
return NULL;
// 特殊情况下,当查找位置小于0或超出主串长度减1时,返回NULL
if (pos < 0 || pos > lenstr - 1)
return NULL;
int* next = (int*)malloc(sizeof(int) * lenSub); // 分配next数组的内存空间
assert(next); // 确保内存分配成功
Getnext(next, Sub, lenSub); // 调用Getnext函数生成子串Sub的next数组
int i = pos; // i表示在主串中的当前查找位置
int j = 0; // j表示在子串中的当前查找位置
// 使用循环进行匹配
while (i < lenstr && j < lenSub)
{
// 若j等于-1,或者主串当前字符和子串当前字符相等,则i和j分别自增1,继续匹配
if (j == -1 || str[i] == Sub[j])
{
i++;
j++;
}
// 若主串当前字符和子串当前字符不相等,则将j的值更新为next[j],继续匹配
else
{
j = next[j];
}
}
free(next); // 释放next数组的内存空间
next = NULL; // 将next指针置为NULL,防止产生野指针
// 若j等于子串长度,说明匹配成功,返回匹配到的位置的指针
if (j >= lenSub)
{
return str + (i - j);
}
// 若匹配失败,返回NULL
return NULL;
}
int main()
{
char str1[] = "abcdefabcdef";
char Sub[] = "aaaaaaaab";
char* ret = KMP(str1, Sub, 0); // 在主串str1中查找子串Sub,从位置0开始查找
if (ret != NULL)
{
printf("%s\n", ret);
}
else
{
printf("子串未在主串中找到\n");
}
return 0;
}
注意:这种情况可以调试一下,查看next
数组是否和我们想的一样进行了优化。
#include
#include
#include
#include
// 改进后的KMP算法中的Getnext函数,用于生成子串Sub的next数组
void Getnext(int* next, char* Sub, int lenSub)
{
// 特殊情况下,当子串长度小于2时,直接设置next[0]为-1
if (lenSub < 2)
next[0] = -1;
else
{
// 初始化next数组的前两个元素
next[0] = -1;
next[1] = 0;
int k = 0; // k表示最大相同前后缀的长度
int i = 2; // i表示当前需要计算next值的下标
// 使用循环计算next数组的值
while (i < lenSub)
{
// 若k等于-1,或者Sub[k]等于Sub[i-1],则next[i]的值为k+1,然后i和k都自增1
if (k == -1 || Sub[k] == Sub[i - 1])
{
next[i] = k + 1;
i++;
k++;
}
// 若Sub[k]不等于Sub[i-1],则将k的值更新为next[k],继续检查
else
{
k = next[k];
}
}
i = 1;
// 进行第二次优化,处理next数组中连续相等的情况
while (i < lenSub)
{
if (Sub[i] == Sub[next[i]])
{
next[i] = next[next[i]];
}
i++;
}
}
}
// 模拟实现strstr函数,用于在主串str1中查找子串str2,返回第一次出现的位置的指针
char* my_strstr(char* str1, char* str2)
{
assert(str1 && str2); // 断言str1和str2非空
int lenstr = strlen(str1);
int lenSub = strlen(str2);
// 特殊情况下,当子串为空串时,直接返回主串的起始位置
if (str2[0] == '\0')
return str1;
// 特殊情况下,当主串为空串时,返回NULL
if (str1[0] == '\0')
return NULL;
int* next = (int*)malloc(sizeof(int) * lenSub); // 分配next数组的内存空间
assert(next); // 确保内存分配成功
Getnext(next, str2, lenSub); // 调用Getnext函数生成子串str2的next数组
int i = 0; // i表示在主串中的当前查找位置
int j = 0; // j表示在子串中的当前查找位置
// 使用循环进行匹配
while (i < lenstr && j < lenSub)
{
// 若j等于-1,或者主串当前字符和子串当前字符相等,则i和j分别自增1,继续匹配
if (j == -1 || str1[i] == str2[j])
{
i++;
j++;
}
// 若主串当前字符和子串当前字符不相等,则将j的值更新为next[j],继续匹配
else
{
j = next[j];
}
}
free(next); // 释放next数组的内存空间
next = NULL; // 将next指针置为NULL,防止产生野指针
// 若j等于子串长度,说明匹配成功,返回匹配到的位置的指针
if (j >= lenSub)
{
return str1 + (i - j);
}
// 若匹配失败,返回NULL
return NULL;
}
int main()
{
char str1[] = "abcdefabcdef";
char str2[] = "\0";
char* ret = my_strstr(str1, str2); // 在主串str1中查找子串str2
if (ret != NULL)
{
printf("子串在主串中的位置:%s\n", ret);
}
else
{
printf("子串未在主串中找到\n");
}
return 0;
}
strstr
函数的区别就在于pos
参数和当子串是\0
时,strstr
函数会直接返回主串的起始地址,我们略做修改,就是模拟实现strstr
函数KMP算法版本,并且它的时间复杂度只有 O ( m + n ) O(m+n) O(m+n)。