对于这一个问题:问字符串S中有多少子串与字符串M相同;即
S : abbaeababbae
M: bbae
可以看出来 S中有两个子串与M相同
我们可以采用最暴力的算法进行计算:
如图:
a | b | b | a | e | a | b | a | b | b | a | e |
b | F | ||||||||||
b | b | a | e | T | |||||||
b | b | F | |||||||||
b | F |
........
我们将M的每一位与S对应的位置进行比较,比较完后判断是否相等,然后进行"移动";
这样就可以统计完S里有多少个M
代码如下:
//S=abbaeababbae
//M=bbae
std::string s="abbaeababbae"
std::string m="bbae"
int cnt=0;
for(int i=0;i<s.size()-m+1.size();i++){
bool flag=true;
for(int j=0;j<m.size();j++){
if(s[i+j]!=m[j]){
flag=false;
break;
}
}
if(flag==true)cnt++;
}
可以看到:我们需要移动 n-m次 比较 (1 ~ m )次 最差情况的时间复杂度就是O(n*m)
接下来介绍的kmp的思想由于文章不能很生动的表示
推荐看这个up的视频:「天勤公开课」KMP算法易懂版_哔哩哔哩_bilibili
了解完kmp后接下来介绍next数组的实现;
我们记S为主串,M为模式串;设字符数组的索引从1开始; 使M=" "+M;
next 数组就是一个预处理
定义:1:next[j]值 为 下一次比较时在模式串的第 next[j] 位 进行比较(怎么用next)
2: next[j]= j之前的最长公共前后缀长度+1;(j之前不包含j)规定next[1]=0;
我们来看看next数组怎么用的,再来看怎么不直接用定义实现next数组;
还是看前面的图:
先根据定义 手算一下next数组:next[j]= j之前的最长公共前后缀长度+1;
next[1]=0; (规定)
不可以认为 2前面的 b既可以看做前缀和后缀,因为没意义;
得到:next[2]=0+1;
得到next[3]=1+1
b!=a bb!=ba;
得到next[4]=0+1;
我们还要求一下next[5]: 同样可以得到next[5]=0+1;
所以M的next数组为:next[1]=0,next[2]=1,next[3]=2,next[4]=1,next[5]=1;
假设我们通过代码已经求出了next数组:
再来求S:
主要的思路是j指向的i的位置 s[i]!=M[j]就调用next数组
模式串的第一位与主串不同 next[1]=0 M直接往后移一位
cnt++;
比较相同的话j往后移一位;j移到了M的后面 即j=5;
next[j]==next[5]==1;
模式串移到的位置使得: M[ next[j] ]为j指的位置(这个和next[1]并不一样)
---->
再进行比较:
next[1]:直接后移
j==2 : next[2]=1;-->
j==1 next[1] :-->
cnt++;
这是时候j 指向的i到了主串的后面比较结束;
知道了怎么用next数组后现在来看怎么得到next数组:
如果我们按照定义求完最长公共前后缀 ,加个一得到next[j]的话,效率就不太高了
接下来介绍一个高效的方式,通过递推得到next数组 next[1]=0;
因为next数组只与模式串有关所以下面只例举模式串
来看这个模式串
这里要递推得到next[j+1]的话就要知道next[1],next[2].....next[j];
这个模式串的next[]={x,0,1,1,1,2,1,1,2,3,4,5,6,7,8};next[0]可以不管;
最后一项next[14]=8;再看下图:
我们如何根据字符 'x' 以及 j+1前面的 next 求出来next[j+1]呢?
对了next有一个性质:next[j+1]<=next[j]+1;(因为就往后推了一位,所以无论如何next的每一项都不会比前一项大于2)
通常采用假设法:
此时j==14
if X=="B"( M(8) ) :
next[15]=8+1
else:
令 j=8; next[8]==2
如图: 因为:由next[15]知道:M(1~7)==M(7~13) ;又由next[8]知道: M(1)==M(7); (后面的同理)
得到M(1)==M(7)==M13)
因此有:
if X=="B"( M(2) ):
next[15]=2+1 看图解释:
else:
令:j=2
if: X=="A"(M(1)):
next[15]=1+1
else:
next[15]=0+1 (因为第一个和他不同那么前后缀为0)
就这样无论x等于什么next[15]总能通过找到一个答案;
那么这个代码是怎么写的呢?总不能套一堆if else 吧:
我们从上面的 的带颜色的句子截取下来:
你会发现现那个 令j=...似乎毫无逻辑却又非常有规律
这便是这个next数组求解的神奇之处
若想知道为什么可自行百度它的数学推理
要求next[15]
j==14: next[14]=8;
if x==M[8]
next[15]=next[14]+1
else
j=8: next[8]=2
if x==M[2]
next[15]=next[8]+1
else
j=2 next[2]=1
if x==M[1]
next[15]=next[2]+1;
else
next[15]=1 || next[15]=next[1]+1
这看起来像动态规划还是像递归?
接下来的代码是从next[1]开始递推推到next[M.size()]
看代码:
//M为模式串:
void getnext(string& M,int*next){
int i=1,j=0;next[1]=0;
while(i<M.size()){
if(j==0){
next[i+1]=1;
j=1;
i++;
}
else if(M[i]==M[j]){
next[i+1]=j+1;
i++;
j++;
}
else{
j=next[j];
}
}
}
//--------------------------------------------
//极简代码:
void getnext(string& M,int*next){
int i=1,j=0;next[1]=0;
while(i<M.size()){
if(j==0||M[i]==[j])next[++i]=++j;
else j=next[j];
}
}
时间复杂度:next 玄学;最差是线性;
与主串比较(N+M)
求出这个next就可以一劳永逸了~
最后再附上一句经典:
一个人能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。
————KMP