kmp算法已经学过好多次了,但每一次都会忘记,因此打算在这里借着这一道模板题记录下自己对kmp算法的理解
kmp算法的核心思想可以理解为:利用目标串自身的信息,来减少匹配次数提高效率
- next数组的引入
next数组就是用来帮助利用目标串自身的信息的。我们这里定义next数组表示的是字符串的最长公共前后缀长度。那么对于字符串ABABABAC来说,求得其next数组就如下所示
模式串 | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
next数组 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 0 |
相信通过手工求next数组大家都能够理解,难点在于理解代码求next数组。
- 求解next数组代码分析
void getnext(char s[],int next[])
{
//求next数组的关键要利用目标串的信息
int len = strlen(s);
next[0]=0;
for(int i=1,k=0;i<len;i++)
{
while(k>0 && s[k]!=s[i]) //利用已经匹配好的信息
k=next[k-1];
//当前k是0~i-1的字符串最长前后缀长度。
//等价于 k=next[next[i-1]-1]
if(s[k]==s[i])
k++;
next[i]=k;//得到第i个元素的最长公共前后缀长度
}
}
#我们来分析一下为什么这样可以计算next数组的值。我觉得最关键的是:
我们来模拟一下当i等于7的时候,此时k应该为5,我们将s[7]与s[5]进行比较,发现不相等,那么s[7]接下来该与谁比较呢?
对任何i,进入这步该比较的下标是 next[next[i-1]-1],而next[i-1]=k,所以新比较的下标应该是 k=next[k-1],如果k为0就不能再往前跳,跳出循环进行比较,相等为1,不等为0。
因此s[7]与s[next[4]] 即s[3]比较,
#因为下标到6时,子串有最长公共前后缀ABABA,如果s[7]=B=s[k]=s[5],那么显然next[7]=6 但是不等。这个最长公共前后缀ABABA的最长公共前后缀为3,若s[7]==s[3],则可以保证s[0-4]为这个串的最大公共前后缀。
- kmp算法主要代码
for(int i=0;i<mainl;i++){
while(j>0 && mainstring[i]!=substring[j]){
j=nexts[j-1]; //while循环很关键。保证主串的下标不变,利用目标串已经匹配好的数据
}
if(mainstring[i]==substring[j])
j++;
if(j==subl){
j=nexts[j-1];
ans++;
}
}
使用已匹配部分的子串的特征来减少移动量
主串M为ABABABCCCC,目标串T为ABABC
M[4]!=T[4],由于已经匹配的ABAB的next值为2,下一步直接比较M[4]和T[2]就好了,就相当于T向右移动了两次。
- 全部代码
#include <iostream>
#include <string>
#include <algorithm>
#include <cstring>
#include <stdio.h>
using namespace std;
const int maxn=10005;
const int maxx=1000005;
int nexts[maxn];
char mainstring[maxx],substring[maxn];
void getnext(char s[],int next[])
{
//求next数组的关键要利用目标程的信息
int len = strlen(s);
next[0]=0; //我这里next 定义从0开始
for(int i=1,k=0;i<len;i++)
{
while(k>0 && s[k]!=s[i]) //利用已经匹配好的信息
k=next[k-1];
//当前k是0~i-1的字符串最长前后缀长度。
//等价于 k=next[next[i-1]-1]
if(s[k]==s[i])
k++;
next[i]=k;
}
}
int main() {
int n;
scanf("%d",&n);
while(n--) {
memset(nexts,0,sizeof(nexts));
scanf("%s%s",substring,mainstring);
getnext(substring,nexts);
int ans=0;
int j=0;
int mainl=strlen(mainstring),subl=strlen(substring);
for(int i=0;i<mainl;i++){
while(j>0 && mainstring[i]!=substring[j]){
j=nexts[j-1]; //while循环很关键。保证主串的下标不变,利用目标串已经匹配好的数据
}
if(mainstring[i]==substring[j])
j++;
if(j==subl){
j=nexts[j-1];
ans++;
}
}
printf("%d\n",ans);
}
return 0;
}
5.后话
其实比较普遍的方法是将next[0]=-1,next[n]定义为0-n-1这部分子串的最大公共前缀和。刚开始我是觉得next[0]好理解一些,总体上来说其实算法的思路都一样的。