部分参考了 BLOG
kmp算法的精髓就在于next数组,从而达到跳跃式匹配的高效模式。
而next数组的值是代表着字符串的前缀与后缀相同的最大长度,(不能包括自身)。
"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;
"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
这里举个例子:
这样我们就求出来了next数组
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | 0 | 1 | 2 | 3 | 1 |
当前后缀特别长的时候(假设该串大小为1000),我们当然不可能挨个去比较前后缀,next数组怎样用代码去求呢?
仍然是上面那个例子
A B A B A A
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | ? | ? | ? | ? | ? | ? |
此时next数组的值,全为未知,
1.初始化,next[0]为0;
因为next[0]代表t[0]~t[0]即"X","X"的前缀和后缀都为空集,共有元素的长度为0.
所以无论X的值为任意值,next[0]=0;
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | ? | ? | ? | ? | ? |
2.求解next[1],
- t[0] != t[1] next[1]为0; 假设此时为XY ,无最大前后缀,故next值为0.
- t[0]==t[1] next[1]为1, 假设此时为XX,最大前后缀为X,故next值为1.
得出next[1]=0;
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | 0 | ? | ? | ? | ? |
3.求解next[2],
- t[0]!=t[2] next[2]为0; 假设此时为XYZ,前缀为 XY,后缀为YZ,无最大前后缀,故next值为0.
- t[0] == t[2] next[2]为1; 假设此时为XYX,前缀为XY,后缀为YX,最大前后缀为X,故next值为1.
在该题中,的next[2]=1
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | 0 | 1 | ? | ? | ? |
4.求解next[3]
在该题中,next[3]代表t[0]~t[3]即"ABAB"的最大前后缀,即"AB",长度为2.
在求next[3]时,如何用代码求
- t[next(3-1)]=t[3]:next[3] = next[next(3-1)+1]+1;
疑问:为什么求next[3]要把t[3]与t[next(3-1)]做对比,
首先我们要明白next[3]是什么,next数组的值是代表着字符串的前缀与后缀相同的最大长度,(不能包括自身),
所以next[3]代表的是t[0]~t[3],这四个字符放入前缀与后缀相同的最大长度
t[next(3-1)]=t[1],为什么要将t[3]和t[1]对比,也就是t[3]与t[next(2)]对比,这是我之前搞不懂的一个地方!!!!
然后看了一些大佬的图解似乎好像仿佛明白了。
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | 0 | 1 | ? | ? | ? |
我们求next[3]的时候,已经求出next[0]~next[2],此时我们知道了next[2]=1,意味着next[2]是代表着字符串的前缀与后缀相同的最大长度为1。
也就是对于ABA来说,前缀与后缀相同的最大长度为1,这是我们已知的条件,现在我们要知道对于ABAB来说,前缀与后缀相同的最大长度为??????
对于ABA来说,前缀"AB",后缀"BA",对于ABAB来说,前缀“ABA”,后缀“BAB”,现在我们观察得出结论
ABAB的前缀和后缀必然分别包含ABA的前缀和后缀,即next[3]字符串的前缀和后缀必然分别包含next[2]的前缀和后缀,
无论该字符串是何值。
既然我们已经知道了next[2]=1,代表着ABA的前缀与后缀相同的最大长度为1,那我们计算next[3]的时候,就知道了第一个字符必然是相同的,所以我们无需再比较第一个字符了,(KMP算法就是为了省这一步!!!!),所以我们将t[3]直接与t[1]而不是t[0]比较,若t[3]=t[1],那我们可以直接说next[3]=2=next[1]+1.
- t[next(3-1)]!=t[3]:怎么办? 看下面,这就是KMP算法的精髓
void makeNext(char s[],int next[])
{
int len = strlen(s);
next[0]=0; //初始化
for(int i=1,k=0;i<len;i++)
{
while(k>0 && s[k]!=s[i]) //这个while是最关键的部分
k=next[k-1];
//等价于 k=next[next[i-1]-1]
//等号右边的k起的只是下标的作用
if(s[k]==s[i])
k++; //相等就+1
next[i]=k; //赋值
}
}
在该题中,t[0]="A",t[3]="B",显然不相等,
所以需要用该算法
如何求next[3]呢,将t[3]与t[next(3-1)]对比
即t[3]与t[next[2]]=t[1]做对比,
此时t[3]="B",t[1]="B",满足t[3]=t[next(3-1)],next[3]=next[t[[next(3-1]+1]+1;
得到t[3]=next[1+1]+1=1+1=2;
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | 0 | 1 | 2 | ? | ? |
PS:这里为了理解可以推广为,若经过KMP算法得到t[3]=t[0],则next[3]=next[0+1]+1,若经过KMP算法得到t[4]=t[2],则next[4]=next[2+1]+1
(可以记忆为---里面加1外面也要加1)
这个时候我们虽然把next[3]计算出来了,但是肯定有人问了,要是t[3]与t[next(3-1)]还是不相等怎么办?????
这个时候我们举另外一个例子
A B A B A B A C
0 1 2 3 4 5 6 7
next[6] = 5
即前缀为t[0]~t[4] A B A B A
后缀为t[2]~t[6] A B A B A
next[4] = 3
即前缀为t[0]~t[2] A B A
后缀为t[2]~t[4] A B A
next[4]的前缀一定是next[6]的前缀
next[4]的后缀也一定是next[6]的后缀
现在我们要求next[7],将t[7]与t[next(7-1) ] 比较(此时t[next(7-1)]=t[5]),发现还是不相等
那么可以将t[7]与t[next(next(7-1)-1)]比较 (此时t[next(next(7-1)-1)]=t[3]),如果相等,则next[7] = next[3+1] +1;
(同前面所述一样,里面加1,外面也加1)
不相等就重复此过程,直到t[7]与t[0]比较.
好了,关于KMP算法我们已经通过例题全部解释了一遍,此时这个例子还没有算完,我们通过KMP算法得出最终结果来验证是否KMP算法是否可靠
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | 0 | 1 | 2 | ? | ? |
这个时候求next[4]
1.判断t[4]与t[next(4-1)]是否相等 ,t[4]="A",t[next(4-1)]=t[2]="A"
2.next[4]=next[2+1]+1=2+1=3
再求next[5]
1.判断t[5]与t[next(5-1)]是否相等,t[5]="A",t[next(5-1)]=t[3]="B";
2.由于t[5]与t[next(5-1)]不相等,所以要继续循环,
2.1 判断t[5]与t[next(next(5-1)-1)]是否相等,t[5]="A",t[next(next(5-1)-1)]=t[next(3-1)]="B";
由于t[5]与t[next(next(5-1)-1)]还是不相等,继续循环
2.1.1 判断t[5]与t[next(next(next(5-1)-1))-1]是否相等,t[5]="A",t[next(next(next(5-1)-1))-1]=t[next(1-1)]=t[0]="A";
此时满足t[5]与t[next(next(next(5-1)-1))-1]相等,所以next[5]=next[0+1]+1=1(记忆口诀为里面加去,外面也加1)
模式串t | A | B | A | B | A | A |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | 0 | 0 | 1 | 2 | 3 | 1 |
这个时候我们就得到了完整的表格
与我们用非代码计算出来的结果对比
可以发现,用KMP算法所得的结果是可靠的。
下面附上kmp完整代码
#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
using namespace std;
void makeNext(char s[],int 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];
if(s[k]==s[i])
k++;
next[i]=k;
}
}
int kmp(char t[],char s[])
{
int len1 = strlen(t);
int len2 = strlen(s);
int next[len2];
makeNext(s,next);
for(int i=0,j=0;i<len1;i++)
{
while(j>0 && t[i]!=s[j])
{
j=next[j-1];
}
if(t[i]==s[j])
j++;
if(j==len2)
return i-j+1;
}
}
int main()
{
char t[]="1234561123458412";
char s[]="611";
cout<<t<<endl;
cout<<s<<endl;
cout<<"下标为"<<kmp(t,s)<<endl;
}