一.前言
笔者经过学习并对KMP算法思考理解,想在本文与各位大佬萌新进行讨论,也算是笔者对这一算法学习的总结,供大家参考,并可以对更简单的理解给予我私信,共同进步。
本文从三个方面对KMP算法进行讨论
1.从BF算法引入KMP
2.next数组的含义
3.求解next数组
二.正文
2.1从BF算法引入KMP
BF算法就是我们说的暴力匹配,我们知道主串中包含了很多个与我们要找的模式串长度相同的子串,所以暴力匹配算法相当于把这些子串都和模式串compare了一次,但是我们在真实的比较过程中不是把所有子串找出来,而是移动指针比较,如图:
我们现在就可以移动P1和P2指针进行比较,当P1和P2相等时,继续向后比较,如果不等那么我们需要把模式串向后移动一位,继续前面的操作如图:
当指针指向P1和P2所示,表示不满足,此时我们认为这个A开头的子串不可能匹配了,就把模式串移动到B开头的子串进行比较,为了方便我后面图把主串令为S1,模式串为S2:
所以只需将模式串按照上面步骤依次比较,直到找到我们想要匹配的子串为止,当满足以下两个条件时结束:
1.当P2等于斜杠0了,证明找到了(有些教材用的是字符串的长度判定)
2.当 P1等于斜杠0,证明找不到
但是细心的朋友比较上面两个图发现,当我重新从B进行比较的时候,P1回到了B的位置,P2回到了A的位置,我们把这个称作指针回溯,每一次都要重新把主串回溯到上一次比较的下一个位置,把模式串回溯到开头。代码供参考
char* my_strstr(const char* arr1,const char* arr2)
{ //加上const是因为我们只需要查找不改变指针内容
if (*arr2 == '\0')
{
return arr1;
}
while (*arr1)
{
char* s1 = arr1; //s1和s2是为了记录回溯位置
char* s2 = arr2;
while ((*s1!='\0')&&(*s1 == *s2)&&(*s2!='\0'))
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return arr1;
}
arr1++;
}
return NULL;
}
int main()
{
char arr1[] = "aaababcdef";
char arr2[] = "abc";
char*p1=my_strstr(arr1, arr2);
if (p1 == NULL)
printf("没有找到");
else
printf("%s\n",p1);
return 0;
}
算法分析:因为我们每个字符子串都要比较一次,所以如果主串长度为m,模式串长度为n的话,我们最坏的打算是要比较(m-n)*n次,时间复杂度为O(m*n)。
如何优化呢: 我们来看一个例子
上图匹配到s1[4]时会退出本次匹配回溯指针从s1[1]也就是B处开始第二次子串匹配,但是我们会发现移动模式串的时候到s1[1],s1[2]都不行,因为我们模式串开头为A,就是说我们必须要遇到开头是A的才有可能匹配成功,意思是说我们比较过的字符中,存在相同的前缀和后缀时,才会去考虑这时候的子串是否可能匹配,意味着我不需要回溯主串的指针,直接拿主串匹配失败的时刻的字符与模式串中的某个元素进行比较,具体是哪个元素,我们把这个元素的位置用一个数组存起来,这个数组就是next[j]。
2.2next的含义
刚刚初步认识了next[j]是什么,是在某个元素匹配失败的时候,主串指针不动,存储模式串指针回溯的位置的数组,而在匹配成功的时候,模式串和主串已经匹配的字符是相同的,即我们不需要看主串长什么样子,只需要知道模式串就OK:
如ABCDAB,指针在A时候A没有前缀后缀,指针在B时候,B前缀为A,没有后缀,指针在C时候,前缀为A,后缀为B,指针在D时候,前缀为A,AB,后缀为C,BC,笔者用枚举的方式解释前后缀,为了避免匹配过程中遗漏掉可能成功的子串,要找到匹配失败前的最长相同前后缀。
next[j]的双重含义:
1.next[j]表示匹配失败时该字符前面最长的相同前后缀,相同前后缀用K计数;
2.next[j]表示匹配失败后,模式串应该回溯的指针位置。
拿模式串ABCDABE,在E匹配失败时,前面有最大长度为2的相同前后缀AB,相当于我们要把AB向后移动:
直接用s2[2]与s1[6]比较,上述的next含义是规律,读者不必纠结为什么会有这个规律,规律是拿来发现的,不是拿来创造的。
此时因为不必回溯s1的指针,算法变得很简单了,时间复杂度为O(m+n)。
2.3next数组的求解
先看这一个问题如果我们已知了上一个字符的next[j]值,怎么求下一个呢,有两种情况:
1.char S[ ]="ABCDABE",对于字符串S来说,比如我们知道S[5]时next的值为1,那S[6]的next的值可以直接比较S[1]与S[5]就ok,如果相等,那么S[6]next的值就是前一个next值加1,即为2
2.当S[1]与S[5]不相等时,看个图
指针在P2位置,红色部分是P2之前的相同前后缀,我们发现红色部分完美的关于ED对称,此时有K个相同的前后缀,K为6,即next[j]为6,如果想要知道P2之后的D的next,我们需要拿P2和P1比较,发现P2不等于P1,证明了找不到长度更长的相同前后缀,只能找更短的,但要使得要和E前面的部分要相同
当P1指针跳到P3时候,绿色部分刚好与E之前的部分相同,这就是next数组的含义了,它表示当前字母最长的相同前后缀,并且指向对应的位置,我们再比较P2和P3,P2和P3相同,相当于P2后面D的next值就为,P1处的next值加1就行了,此时next为3,如果P3和P2还不相等,那么我们就只能继续找P3的next处的值继续比较,原理很简单,因为相同的前后缀一定对称,比如P2前的红色框是关于ED的完美翻折。
重复以上步骤,它就是一个递归的思想代码如下
int my_KMP(char* des, char* sre,int* next)
{
int slen = strlen(des);
int plen = strlen(sre);
int i = 0;
int j = 0;
while (i < slen && j < plen)
{
if (j == -1 || des[i] == sre[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j == plen)
{
return i - j;
}
else
return -1;
}
void get_next(int next[], char*sre )
{
int len = strlen(sre);
int j = 0;
int k = -1; //因为j==0没有前缀,所以为了方便,K初始值为-1
next[0] = -1; //同样的把next数组的初始值也令为-1
while (j < len - 1)
{
if (k == -1 || sre[k] == sre[j]) //sre[k]表示前缀,sre[j]表示后缀
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k]; //next数组迭代,K表示的是最长的前后缀长度
}
} //k=next[k]的目的是匹配不成功后找到对称的位置
}
int main()
{
char arr1[] = "abcdefaaaabbbaaccc";
char arr2[] = "aabbba";
int next[100] = { 0 };
get_next(next, arr2);
int p1=my_KMP(arr1, arr2,next);
printf("%d\n", p1);
return 0;
}
里面注意的细节我都标注在代码里面,KMP就是next难找,笔者初学时,脑袋炸了。。。。。
另外有一篇博主写的很详细,我在这个基础上加入了自己的理解,因为写太多了好多时候就懒得去看文字,本文参考链接如下
从头到尾彻底理解KMP(2014年8月22日版)_v_JULY_v的博客-CSDN博客_从头到尾彻底理解kmp
这是个让人膜拜的大佬。