相信大家在遇到字符串匹配问题时,无论是听老师上课讲还是在网上查询资料时几乎都会用到KMP算法,本篇博客借鉴于大博哥对于KMP算法的分析以及自身对于KMP算法的看法,相信认真看完了后会对你有一些帮助。
目录
1 BF 算法
2 KMP 算法
3 KMP算法的优化
了解KMP算法之前,我们先来回忆一下BF算法(暴力求解),基本思想就是主串中元素与子串中元素一一比较,匹配失败就让子串返回到0下标,主串回溯到开始匹配的下一位。
假设主串的长度位M 子串的长度为N BF算法的时间复杂度为 O(M*N)
动图详解(此图借鉴于其他博主):
具体代码:
int BF(char* mainStr, char* subStr,int pos)
{
assert(mainStr && subStr);
if (!*subStr)
{
return 0;
}
if (!*mainStr)
{
return -1;
}
int lenMain = strlen(mainStr);
int lenSub = strlen(subStr);
int i = pos; //遍历主串
int j = 0; //遍历子串
while (i < lenMain && j < lenSub)
{
if (mainStr[i] == subStr[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
}
if (j == lenSub)
{
return i-j;
}
else
{
return -1;
}
}
KMP算法的诞生:KMP算法是三位大牛:Knuth、Morris和Pratt同时发现的,于是取了他们名字的首字母然后组合起来,就成了该算法的命名。(来自百度)
kmp算法的主要思想是:在主串中查找子串时,利用主串和子串中已经匹配的部分,跳过一些无用的比较,使主串的标记指针不会回溯,子串的标记指针移动。其实主要是子串中如果有部分内容和主串中一致,我们需要研究这些已知的匹配内容,这相当于我们需要研究子串,发现子串中存在的规律。KMP算法的时间复杂度为 O(M+N)
听起来是不是有点懵,没关系,我们来举个栗子:
主串:ababcabcdabcde
子串:abcd
由上述主串与子串数据可知目前在索引为2匹配失败,就算让主串回退到1也是没有必要的,因为主串中1位置字符b与子串0位置的a也不一样,所以我们的目的是:主串是不回退的,让子串回退到特定的位置。
那么子串应该回退到什么位置呢? 再来举个栗子:
主串:abcababcabc
子串:abcabc
这里主串与子串在索引为5的位置不匹配,由于主串不回退,回退的是子串,通过肉眼观察我们发现子串回退到索引为2是比较合理的(我们要尽量在主串中找到和子串匹配的一部分串),由于主串中索引为5的字符a!=子串中索引为2的字符c,所以还要继续回退,直到回退到与字符a相等为止。那如果字符数量多了用肉眼观察肯定是不切合实际的,所以我们引出了next数组,用来保存子串中某个位置匹配失败后回退的位置。
KMP的精髓就是next数组,那么应该怎样来求next数组呢?假定next[j]=k;
求k的规则:
- 找到匹配成功部分的两个相等的真子串(不包含本身)一个以下标0开始,另一个以j-1下标结束。
- 总有next[0]=-1,next[1]=0。
有了上面的规则我们就可以来求next数组了:
栗子1:“ababcabcdabcde"
它对应的next数组为:[-1,0,0,1,2,0,1,2,0,0,1,2,0,0]
栗子2:”abcabcabcabcdabcde"
它对应的next数组为:[-1,0,0,0,1,2,3,4,5,6,7,8,9,0,1,2,3,0]
通过上面的栗子我们可以得出总结:找的时候可以找重复的,但是必须是以0下标开始,找的前一位下标结束;而且数组中元素增大只能一个一个的加,减小可以跳跃减小。
到这里我相信大家对求next数组应该没有什么问题了。但现在的问题是我们如何用公式来求next数组,总不能next数组让我们人为来算,这显然是不现实的。
我们假设已知:next[i]=k;
k i
给定一个字符串" a b c a b a b c a b c "
0 1 2 3 4 5 6 7 8 9 10
next数组: [-1,0,0,0,1,2,1,2,3,4,5]
我们假设此时i==8
则我们可以知道:p[0]……p[k-1] == p[i-k]……p[i-1]
所以由:next[i]=k <---> p[0]……p[k-1] == p[i-k]……p[i-1]
如果现在 p[k]==p[i] 将其分别加在上式右边的两边得:
p[0]……p[k] == p[i-k]……p[i]
所以不难推出:next[i+1]=k+1;这也很好得解释了为什么增加只能一个一个的加。
那么问题又来了,如果p[k] != p[i]呢?
假设索引为8的元素由a改为b,那么k就要回退到next[k]的位置,也就是k=0,但还是不满足p[k]==p[i],就继续回退k=-1,此时已经找不到了,就直接将0给予给下一位,也就是:
next[i+1]=0;
好了,next数组的求解概念已经掌握,现在我们如何用代码来将其表示出来呢?
代码的具体实现:
int KMP(char* mainStr, char* subStr, int pos)
{
assert(mainStr && subStr);
if (!*subStr)
{
return 0;
}
if (!*mainStr)
{
return -1;
}
int lenMain = strlen(mainStr);
int lenSub = strlen(subStr);
//动态申请next数组
int* next = (int*)malloc(sizeof(int) * lenSub);
if (NULL == next)
{
exit(-1);
}
GetNext(next,subStr);
int i = pos; //遍历主串
int j = 0; //遍历子串
while (i < lenMain && j < lenSub)
{
if (mainStr[i] == subStr[j] || -1==j)
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j == lenSub)
{
free(next);
return i - j;
}
else
{
free(next);
return -1;
}
}
这里面有一个容易出错的点,当j==-1时也不要忘记了处理,当j==-1时说明回到了索引为0的位置,并且还没有找到与其匹配的项,就让j从索引为0开始与i一一比对。
得到next数组的具体代码:
void GetNext(int* next, int* subStr)
{
assert(next && subStr);
int lenSub = strlen(subStr);
next[0] = -1;
next[1] = 0;
int i = 2;
int k = 0;
while (i < lenSub)
{
if ((-1 == k) || subStr[i - 1] == subStr[k])
{
next[i] = k + 1;
i++;
k++;
}
else
{
k = next[k];
}
}
}
由于这里的next[i]是未知的,而next[i-1]才是已知的,所以上面才是subStr[i - 1] == subStr[k]
next[i] = k + 1; 同理当k==-1时说明到索引为0都还没有匹配的项就把其置为0.
假定给一个这样的字符串: " a a a a a a a a a a g "
它的next数组为: [-1,0,1,2,3,4,5,6,7,8,9]
假设在索引为9的位置匹配失败,则j会不断回退直到j==-1,这样效率就比较低了,有什么办法能够一步到位回退呢?
我们不妨优化一下next数组:为了区别,不妨将将优化后的数组取名为nextval
nextval数组的规则如下:
1 如果回退到的位置和当前字符一样,就写回退那个位置的nextval值;
2 如果回退到的位置和当前字符不一样,就写当前原来的nextval值;
举个栗子:给" a b c a a b b c a b c a a b d a b "
next: [-1, 0, 0, 0, 1, 1, 2, 0, 0, 1, 2, 3, 4, 5, 6, 0,1]
nextval: [-1, 0, 0,-1, 1, 0, 2, 0,-1, 0, 0,-1, 1, 0, 6,-1,0]
具体代码:
void GetNextval(int* nextval, int* subStr)
{
assert(nextval && subStr);
int lenSub = strlen(subStr);
nextval[0] = -1;
nextval[1] = 0;
int i = 2;
int k = 0;
while (i < lenSub)
{
if ((-1 == k) || subStr[i - 1] == subStr[k])
{
nextval[i] = k + 1;
i++;
k++;
}
else
{
k = nextval[k];
}
}
i = 2;
while (i < lenSub)
{
if (subStr[i] == subStr[nextval[i]])
{
nextval[i] = nextval[nextval[i]];
}
i++;
}
}
好了,KMP算法就到这里了,如果有什么不对的地方欢迎各位佬在评论区指正。