前言:今天我们来学习两种算法,BF算法和KMP算法。相信会让许多小伙伴们打开新世界的大门。
1 BF算法
实践是检验真理的唯一标准。举一个例子说明BF算法。现在我们要在一个主串中找子串的位置。那我们该如何解决这个问题呢?最简单的办法自然是使用C语言中的库函数strstr。
#include<stdio.h>
#include<string.h>
int main()
{
char str1[20] = "abbbcdef";
char str2[20] = "bbc";
char* ret = strstr(str1, str2);
char* ps = strncpy(ret, "hello", 5);
printf("%s\n", str1);
return 0;
}
但现在我们不使用C语言中的库函数strstr,是否有办法可以解决呢?当然是有的。
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* BF(char* str1, char* str2)
{
assert(str1 != NULL && str2 != NULL);
char* s1 = str1;//遍历主串
char* s2 = str2;//遍历子串
char* cur = str1;//记录可能开始匹配的位置
while (*cur)
{
//完成一次匹配
s1 = cur;
s2 = str2;
while (*s1 && *s2 && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return cur;
}
cur++;
}
return NULL;
}
int main()
{
char str1[20] = "abbbcdef";
char str2[20] = "bbc";
char* ret = BF(str1, str2);
char* ps = strncpy(ret, "world", 5);
printf("%s\n", str1);
}
画图分析
*s1=a,*s2=b。此时 *s1 != *s2,说明从cur这个位置匹配失败,那么下一次就要从cur++的位置开始匹配,同时将s1的位置更改为新的cur
。
*s1=b,*s2=b。此时*s1==*s2,说明从当前cur的位置是有可能匹配成功的。那么就执行s1++,s2++的操作,比较下一对字符的内容
。
*s1=b,*s2=c。此时*s1!=*s2,说明从cur的位置匹配失败,进一步更新cur的位置,同时将s1的位置更新为新的cur的位置,s2回到起始位置。重新进行下一次的匹配
。
更新之后新的cur和s1的位置以及s2的位置
。
这里省略了相同的步骤,直接演示最后的结果。
这就是所谓的BF算法。大家是否理解了呢?接下来就让我们一起来学习更加高深莫测的KMP算法。大家做好准备哦。
2 KMP算法
依然是在主串中找寻子串的位置。这一次使用KMP算法实现。
什么是KMP算法呢?
KMP算法是一种改进的字符串匹配算法。KMP算法的核心思想是利用主串与模式串(子串)匹配失败后的信息,尽量减少模式串(子串)与主串匹配的次数以达到快速匹配的目的。具体实现是通过一个next数组实现,数组本身包含了子串的局部匹配信息。KMP算法的时间复杂度O(m+n)
。
KMP算法与BF算法的区别是:主串的i并不会回退,子串的j不一定回退到0位置
。
画图分析
从当前 i 的位置开始匹配,匹配成功就执行 i++,j++的操作,接着匹配下一对字符的内容
。
这里省略了相同的步骤,直接演示最后的结果。此时匹配失败,i不会进行回退,j回退到一个随机的位置
。
该回退到哪里呢?这是一个值得深思的问题。在这个位置匹配失败就意味着 i 前面和 j 前面有一部分是相同的。那我们来研究一下 j 到底该回退到哪里呢?
假设1与3的内容相同,2与3的内容相同。那么1与2的内容就相同。如果此时下标 i 与下标 j 的内容不匹配,j 应该回退到哪里去呢?j 应该回退到子串中下标为2的位置去
。为什么呢?还记得KMP算法的核心思想吗。需要尽量减少模式串与主串匹配的次数以达到快速匹配的目的
。1与3的内容相同,就没有必要再进行重复的匹配,因此 j 应该回退到子串中下标为2的位置。如果依然匹配失败,就继续回退。
那新的问题又来了,我们要怎么得到回退的位置呢?根据上面的图分析,假设子串叫做p,如果p[0]~p[1]之间的内容等于p[3]~p[4]之间的内容,我们会发现相等部分的长度为2,刚好是 j 要回退的位置
。那不就出来了。next数组的作用就是保存子串中某个位置匹配失败后,要回退的位置
。
KMP的精髓就是next数组,也就是用next[j]=k来表示,不同的j表示不同的k值,这个k就是你将来要移动的j要移动的位置
。
k值的求法:
.
找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标0的字符开始,另一个以下标为 j-1 的字符结尾。这两个真子串的长度就是k值
。
.
不管什么数据,next[0]=-1,next[1]=0
。
接下来的问题就是已知next[j-1]=k,如何求next[j]=?
next数组的特点:相邻的k值如果是增加的,一定是逐步加1的过程
KMP算法的实现
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
//str代表主串
//sub代表子串
//pos代表主串开始匹配的位置
void GetNext(int* next, char* sub, int LenSub)
{
assert(next != NULL && sub != NULL && LenSub);
next[0] = -1;
next[1] = 0;
int i = 2;
int k = 0;//保存前一项的k值
while (i < LenSub)
{
if (-1 == k || sub[i - 1] == sub[k])
{
next[i] = k + 1;
i++;
k++;
}
else
{
k = next[k];//有可能回退到-1,子串进行访问时会出现越界访问
}
}
}
int KMP(char* str, char* sub, int pos)
{
assert(str != NULL && sub != NULL);
int LenStr = (int)strlen(str);
int LenSub = (int)strlen(sub);
if (0 == LenStr || 0 == LenSub)
{
return -1;
}
if (pos < 0 || pos >= LenStr)
{
return -1;
}
int* next = (int*)malloc(sizeof(int) * LenSub);
if (NULL == next)
{
perror("空间开辟失败的原因是");
return -1;
}
GetNext(next, sub, LenSub);
int i = pos;//遍历主串
int j = 0;//遍历子串
while (i < LenStr && j < LenSub)
{
if (-1 == j || str[i] == sub[j])
{
i++;
j++;
}
else
{
j = next[j];//有可能回退到-1,子串进行访问时会出现越界访问
}
}
if (j >= LenSub)
{
return i - j;
}
else
{
return -1;
}
free(next);
next = NULL;
}
int main()
{
printf("%d\n", KMP("abbbcdefg", "bbc", 0));//2
printf("%d\n", KMP("abbcdef", "ab", 0));//0
printf("%d\n", KMP("abccdef", "bcd", 0));//-1
printf("%d\n", KMP("abbcdabcdef", "abc", 0));//5
printf("%d\n", KMP("abbccddef", "abcde", 0));//-1
printf("%d\n", KMP("abcddef", "ab", 0));//0
return 0;
}