先了解一下kmp的原理吧
没有耐心看文字的也可直接移步b站哦~
手算版:「天勤公开课」KMP算法易懂版_哔哩哔哩_bilibili
代码版:KMP算法之求next数组代码讲解_哔哩哔哩_bilibili
目录
(一)什么是kmp?
在主串中快速找到所需子串的起始位置。时间复杂度O(m+n)。比暴力好太多...
解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。
(二)几个概念
公共子串:从左边开始模式串与主串相等部分的串
公共前后缀:公共子串中左右两端相同的字符串,前者为前缀,后者为后缀,注意是在比较指针以左,如果有多对公共前后缀,就取最长的,但不能超过公共子串。
上图所示AB是最长公共前后缀,下面看一个更长的ABA是最长公共前后缀,前后缀可以交叉但不能完全重叠。
(三)推理核心步骤
- 将模式串后移,使得前缀移到后缀所在位置。
- 比较指针再次移动,再次移到不等的位置
- 再次找最长公共前后缀
(1)为什么公共前后缀必须在公共子串的两端,AB为什么不行?
因为当模式串右移时,主串的A与模式串的B不对应,不能满足要求。因此AB不是公共前后缀
(2)为什么公共前后缀要找最长的?
按移动的距离来看,最长子串移动距离更短,如果移动后的位置就是最终结果,那么很显然,第一个才是所需子串第一次出现的位置。
- 继续右移,直到找到对应下标或模式串尾部超出主串尾部。
(四)手算next数组
但是在计算机中,串并不能移动,接下来就用计算机更能理解的方式进行处理,也就是指针(或者下标)的变化这里idx1指主串的比较指针,idx2指模式串中比较指针所在位置,length指最长公共前后缀长度。
先把规律告诉大家:idx2 = length + 1
接下来进行推导,算法的本质其实与主串关系不大,因此我们需要对模式串进行处理分析。(这里从1开始计数)。假设初始值idx1=idx2=1;
以这个数组为例:
- 如果模式串的1号位与主串当前位不匹配,就让模式串1号位与主串下一位比较
- 如果模式串的2号位与主串当前位不匹配(前面的都匹配,从第二步开始都是,后面不再赘述),此时最长公共前后缀长度为0,就让1号位与主串当前位比较,相当于模式串向后移了一位。
- 如果模式串的3号位与主串当前位不匹配,此时最长公共前后缀长度为0,就让1号位与主串当前位比较,处理方式与2一致。
- 如果模式串的4号位与主串当前位不匹配,此时最长公共前后缀为A长度为1,就让2号位与主串当前位比较,相当于1号位后移到3号位,第4号位正好是原来2号位的位置.
- 如果模式串的5号位与主串当前位不匹配,此时最长公共前后缀为AB长度为2,就让3号位与主串当前位比较,相当于1号位后移到3号位,第5号位正好是原来3号位的位置.
- 如果模式串的6(idx1=6)号位与主串当前位不匹配,此时最长公共前后缀为ABA长度为3(length=3),就让4(idx2=length+1)号位与主串当前位比较,相当于1号位后移到3号位,第6号位正好是原来4号位的位置.经过推理得出模式串中比较指针所在位置=最长公共前后缀长度+1(当起始值为0时也可以表示为前缀最后一位的下标)
- 继续验证一下:如果模式串的7(idx1=7)号位与主串当前位不匹配,此时最长公共前后缀为A长度为1(length=1),就让2(idx2=length+1)号位与主串当前位比较,相当于1号位后移到3号位,第7号位正好是原来2号位的位置.
- 。。。。。如上推导得出最终结果
- 可以看到除了第一列,其他列除了数字以外都相同,因此我们把第一列设置为方法0,其他列结合上面的数组下标,按数字i标记为方法i存到数组中。
这就是next数组。
有一些规律:
1、next[j]的值 每次最多增加1
2、模式串的最后-位字符不影响next数组的结果
(五)代码模板
没有耐心看完的直接移步b站吧!up讲得很好KMP算法之求next数组代码讲解_哔哩哔哩_bilibili
接下来开始盘代码:虽然手算很容易 但是代码看不懂啊,呜呜呜我看了第四遍才看懂的这个代码。。
1.next数组
- 初始化next[1]=0;i为比较指针下标,j为最长公共前后缀长度,ch,p为模式串,length为模式串长度
- while递归求出p[1]~p[i]最长公共前后缀长度
- 如果p[i]与p[j+1]相等,那么最大公共前后缀长度加1,不等的话回溯推导。
int GetNext(char ch[],int length){
next[1]=0;
int i = 1, j = 0;
while(i<=length){
if(j==0||ch[i]==ch[j])next[++i]=++j;
else j=next[j];
}
}
不理解没关系,跟着我推导一遍!或者直接看下面的例子
发现一些规律:
1.首先要看ch[i]和ch[j]是否相等,不相等的话,j=next[j],也就是ch[i]会跟ch[next[j]]进行比较,以此类推。
2.next[j+1]的最大值是next[j]+1
3.如果p[j]!=p[next[j]],next[j+1]可能的次大值为next[next[j]]+1。(这句话可太难懂了)
既然原理这么难懂,就举一个例子吧
坚持一下!
例子很好理解!
我们假设i+1=17,那next[16]即已知,假设是8,那么p[1-7]=p[9-15](由前后缀的性质)。
如果p8=p16,明显next[17]=8+1=9( 对应next[++i]=++j; ),因为前后缀长度加一。如果p8!=p16(此时i=16,j=8,对应ch[i]!=ch[j]),假设next[8]=4( j=next[j]=next[8]=4 ),有以下关系。橙色线框部分字符串完全相同,由前后缀定义和对称性可得。
由此可得,上图中的1和4 相等,如果p16=p4,那么p[1-4]=p[13-16],则next[17]=5
以次类推,若p16!=p4,next[4]=2,则有以下关系:
若p16=p2,则next[17]=2=1=3,否则继续取next[2]=1,next[1]=0.遇到0时还没有结果(对应j==0),next[17]=1 (next[++i]=next[17]=++j=1).
这下差不多明白了。
2.kmp模板代码
和next数组大同小异,一起看看注释吧
//KMP算法,判断pattern是否是text的子串
bool KMP(char text[],char pattern[])
{
int n=strlen(text),m=strlen(pattern); //求字符串长度
getNext(pattern,m); //计算pattern的next数组
int i = 1, j = 0;
while(i<=n){
if(j==0||text[i]==pattern[j])
{
j++;i++;
if(j==m)return true;//pattern匹配完毕,返回true
}
else j=next[j];//不断回退,直到j回到0或text[i]==pattern[j]
}
return false; //执行完text还没匹配成功,说明pattern不是text的子串
}