学习了BF算法之后,我从内心深处明白了一个道理:从哪里跌倒了就含着泪从哪里站起来。
BF算法利用了i指针地回溯,坚持条条大路通罗马的决心,失败之后不会总结经验,简直就是一个实打实的铁憨憨对吧!
由此一来,想必一定会诞生一个更加牛逼地算法对吧,就BF算法回溯i、j指针这个弊端,下面我们来探索后面所谓的看毛片算法。
KMP算法探索
由BF算法匹配过程中,我们不难发现总是由这么一种情况:
在模式串中的某一个(第i个位置)字符与主串不匹配,但是这个字符前的所有字符都是与主串相匹配的,这样使得BF不得不把模式串从头开始和主串的第i-m+1位置为起点的字符又匹配一次,只要中途出现不匹配的情况还得重新开始匹配(真TM的是机器心态啊😤)我们暂且把这种状态称为
S
k
S_k
Sk模式。
S
k
S_k
Sk模式在执行若干次BF算法后又第一次出现类似之前的
S
k
S_k
Sk模式:
在这里我们称后面出现的这种状态叫做
S
S
S
k
_k
k
+
_+
+
1
_1
1模式
我们把两个状态列表观察:
两个状态的相同点:
- i i i都停留在了同一个位置(如图的第5个位置);
- i i i所指位置之前的所有字符(模式串范围类)都对应匹配;
现在我们总结一下由 S k S_k Sk模式变到 S S S k _k k + _+ + 1 _1 1模式这个过程我们究竟干了啥:
很简单地来说,为了和主串匹配成功啊,若是在主串的第 i i i个位置发现模式串不匹配了,我们就想尽可能让这个 i i i指针朝后面移动以达到匹配成功或者找不到和模式串相同的字串这个目的对吧!
两个状态谁更好?
很显然, S S S k _k k + _+ + 1 _1 1状态是由 S k S_k Sk状态经过若干次BF算法之后才得到的,那么不就可以说 S S S k _k k + _+ + 1 _1 1状态离我们想要的结果更近一点。
在这里还要强调的一点就是 S k S_k Sk状态和 S S S k _k k + _+ + 1 _1 1状态之间没有比 S S S k _k k + _+ + 1 _1 1状态更好的状态了,因为两者之间的状态要么是模式串第一个字符就不匹配直接OUT的状态,要么就是模式串第1~(j-1)字符与主串匹配,第j个不匹配的状态,这两种状态都还需要若干次匹配,所以说 S S S k _k k + _+ + 1 _1 1状态是跟好的状态。
KMP算法的提出
既然说 S S S k _k k + _+ + 1 _1 1状态比 S k S_k Sk状态更好,那么要是我们能把 S k S_k Sk状态和 S S S k _k k + _+ + 1 _1 1状态之间的那些状态省略掉,直接从 S k S_k Sk状态变到 S S S k _k k + _+ + 1 _1 1状态,那不就是美滋滋的事儿吗?(来自计算机的笑逐颜开🤣)
说到底我就是想要把前面那个憨憨BF算法的过程变称变成重复的由 S k S_k Sk状态推进 S S S k _k k + _+ + 1 _1 1状态的过程。
对照表格再深度思考
S
k
S_k
Sk状态和
S
S
S
k
_k
k
+
_+
+
1
_1
1状态之间过程
我们得出下面的结论:
- 从 S k S_k Sk状态变到 S S S k _k k + _+ + 1 _1 1状态其实就是模式串从主串的第一个位置开始对齐移动到从主串第三个位置开始对齐的过程
- S k S_k Sk状态下,模式串第5个位置之前所有字符与主串匹配,通过模式串第5个位置之前的字串我们就知道接下来模式串需要怎么移动,所以可以忽略主串的存在。
把上述结论用表格呈现出来:
我们把主串第五个位置之前的字串定义为F串,并在F串左部和右部再找到两个相等的字串分别叫做前缀和后缀。
那么KMP算法可以简单地描述为:
- 我们只需要看模式串中第 i i i(如本例中的第5)个不匹配位置之前的字串(F串)就能知道接下来模式串怎么移动;
- 由 S k S_k Sk状态变到 S S S k _k k + _+ + 1 _1 1状态,只需要将模式串移动,使得F前缀和F后缀相等就行;
当模式串不重合点前面出现若干对前、后缀怎么办?
我们必须分别取最长的两个串作为前缀和后缀
为社么呢?因为再模式串移动过程中,最长前缀和后缀先发生了重合,之后才轮得到相对短的串重合。
再回到我们找前后缀的目的上来说,就是为了使这个模式串从 S k S_k Sk状态变到 S S S k _k k + _+ + 1 _1 1状态, S S S k _k k + _+ + 1 _1 1状态要求的是第一次出现类似 S k S_k Sk状态的情形。
若是我们取较短的串作为前缀和后缀,那么就是由
S
k
S_k
Sk状态变到
S
S
S
k
_k
k
+
_+
+
n
_n
n状态,之前的那个
S
S
S
k
_k
k
+
_+
+
1
_1
1状态或许能解决
S
k
S_k
Sk状态中不匹配的可能就被我们忽略了。
next数组
通过上面的探索我们发现,我么们完全可以忽略主串,站在模式串的角度上面来考虑问题。
那么现在我们就需要考虑是否能不比较儿直接把
S
k
S_k
Sk状态跳到
S
S
S
k
_k
k
+
_+
+
n
_n
n状态呢?
仔细想想是不可能不比较的了,但是我们似乎可以让这个比较的次数变得很少很少。
思路:要是我们可以知道这个模式串的某个位置发生不匹配的时候, j j j指针需要向模式串的哪个位置移动就好了。
在这里我们把模式串的每个位置发生不匹配后 j j j指针需要移动的位置都做成一张表,那么下次这个位置发生不匹配的时候我们就直接可以不比较而直接跳到下一个状态,仅仅这一张表就解决了后面因为匹配而花费大把时光比较的买卖你说话不划算嘛!
next数组定义
模式串中第 j j j个位置与主串中第 i i i个位置发生不匹配时,应从模式串中的第 n e x t [ j ] next[j] next[j]位置与主串的第 i i i个位置开始比较。
next数组的(手工)求法:
本身这个考研乃身外之物对吧,但在这不得不提一句就是,这个next[]数组的求法也是考研的常考点。
- n e x t [ 1 ] next[1] next[1]为0,为特殊标记,模式串的第一个字符应该和主串当前不匹配字符的下一个字符开始比较;
- n e x t [ j ] next[j] next[j]的值为F前缀或者F后缀的长度+1,注意:F前、后缀取最长;
例子:
KMP算法(代码篇)
KMP算法:
int KMP (Str str ,Str substr,int next []) {
int i = 1,j = 1;
while ( i < = str.length&&j<=substr.length) { //大前提
if (j==0||str.ch[i]==substr.ch[j]) { //j可能为0,为特殊标记,模式串的第一个字符应该和主串当前不匹配字符的下一个字符开始比较;
++i;
++j;
}
else {
j==next[j];
}
}
if(j>substr.length ) {
return i-substr.length; //返回开始匹配成功的首字符位置
}else{
return 0;
}
}
next数组求法(代码篇)
对于特别长的模式串,我们怎么来求它的next数组呢?
在这里我们模拟上面的红色和黄色部分一一对应,此时
n
e
x
t
[
j
]
next[j]
next[j]的值为
t
t
t,那么我们可以分为两种情况来推导
n
e
x
t
[
j
+
1
]
next[j+1]
next[j+1]的值:
- 若 P j = P t P_j=P_t Pj=Pt,则 n e x t [ j + 1 ] = t + 1 = n e x t [ j ] + 1 next[j+1]=t+1=next[j]+1 next[j+1]=t+1=next[j]+1;
- 若 P j ≠ P t P_j≠P_t Pj=Pt,则循环地把 t t t的值赋值为 n e x t [ t ] next[t] next[t],直到 t = 0 t=0 t=0或者满足 P j = P t P_j=P_t Pj=Pt为止,当 t = 0 t=0 t=0时, n e x t [ j + 1 ] = 1 next[j+1]=1 next[j+1]=1。
上面第二种情况类似于KMP算法中由 S k S_k Sk状态跳到 S S S k _k k + _+ + n _n n状态的过程。
代码实现:
void getNext ( Str substr ,int next[]) {
int j =1,t=0;
next[1] = 0;
while (j<substr.length) {
if (t==0||substr.ch[j]==substr.ch[t]) {
next [j+1] = t+1;
++t;
++j;
}else {
t = next[t];
}
}
}
nextval数组
讲真的我没想到上面的next数组竟然还能改进。
当遇到特殊的模式串的时候,比如AAAAAB这种模式串的时候,假如该模式串在第5个位置与主串发生了不匹配,那么
j
j
j是不是就要跳到
n
e
x
t
[
j
]
=
4
next[j]=4
next[j]=4的位置在比较,但我们发现
j
=
4
j=4
j=4的位置上的字符与
j
=
5
j=5
j=5位置上的字符是一样的,同样以此类推,一直到
j
=
1
j=1
j=1都是与主串不匹配的。直到
j
j
j被赋值为
n
e
x
t
[
1
]
=
0
next[1]=0
next[1]=0的时候,
i
i
i指针才向后移动。
那么计算机就不解地发出了疑问:你咋不直接跳到
j
=
1
j=1
j=1呢?
仔细想想,我们确实在憨憨的路上一去不复返了。
下面总结出了求解nextval数组的方法:
- 当 j = 1 j=1 j=1时, n e x t v a l [ ] nextval[] nextval[]赋值为0,作为特殊标记;
- 当 j > 1 j>1 j>1时,若 P j Pj Pj不等于 P P P n _n n e _e e x _x x t _t t [ _[ [ j _j j ] _] ],则 n e x t v a l [ j ] nextval[j] nextval[j]等于 n e x t [ j ] next[j] next[j];
- 若 P j Pj Pj等于 P P P n _n n e _e e x _x x t _t t [ _[ [ j _j j ] _] ],则 n e x t v a l [ j ] nextval[j] nextval[j]等于 n e x t v a l [ n e x t [ j ] ] nextval[next[j]] nextval[next[j]];
上代码:
由next数组改进
void getNextval ( Str substr ,int next[], int nextval []) {
int j =1,t=0;
next[1] = 0;
nextval[1] = 0;
while (j<substr.length) {
if (t==0||substr.ch[j]==substr.ch[t]) {
next [j+1] = t+1;
if (substr.ch[j+1] != substr.ch[next[j+1]]) {
nextval[j+1] = next [j+1];
}else {
nextval [j+1] = nextval[next[j+1]]
}
++t;
++j;
}else {
t = nextval[t];
}
}
}
我们发现上面代码中的next[]竟然没用了,所以完全可以去掉!!!
nextval最终代码
void getNextval ( Str substr , int nextval []) {
int j =1,t=0;
nextval[1] = 0;
while (j<substr.length) {
if (t==0||substr.ch[j]==substr.ch[t]) {
if (substr.ch[j+1] != substr.ch[next[j+1]]) {
nextval[j+1] = next [j+1];
}else {
nextval [j+1] = nextval[next[j+1]]
}
++t;
++j;
}else {
t = nextval[t];
}
}
}
传说中的看毛片算法就此完结,回去继续啃一下严奶奶的书巩固一下,不得不佩服一下发明这些算法的大佬,膜拜D.E.Knuth,J.H.Morris和V.R.Pratt!🐒