文章目录
说实话现在网上的 K M P KMP KMP 讲稿对于我这样的蒟蒻还是太不友好了,里面基本上只写到 怎么做 ,但不告诉你 为什么这么做是对的 ,所以理解起来会有点云里雾里,其实只要理解了本质你会发现 K M P KMP KMP 其实很简单。
暴力匹配
我们用 T = a a b a a c T=aabaac T=aabaac 去匹配 S = a a b a a b a a c S=aabaabaac S=aabaabaac 作例子(下标都是从 1 1 1 开始的)。
第一次匹配:
第二次匹配:
第三次匹配:
第四次匹配(找到了):
我们在四次匹配中总共作了 15 15 15 次比较(因为匹配失败也需要一次比较),暴力的最坏复杂度是 O ( n m ) O(nm) O(nm) 的 。
优化:用失配指针匹配
我们考虑如何优化。
我们发现,在暴力匹配时,有很多比较是多余的,比如说,当我们进行了第一次匹配后,第二次匹配其实完全是多余的,因为第二次匹配成功的 必要条件 是 S 2 − 5 = T 1 − 4 S_{2-5}=T_{1-4} S2−5=T1−4 ,然而在第一次匹配时我们得知了 S 2 − 5 = T 2 − 5 S_{2-5}=T_{2-5} S2−5=T2−5 ,而 T 1 − 4 ≠ T 2 − 5 T_{1-4}\not=T_{2-5} T1−4=T2−5 ,所以我们不用比较 S S S 也能知道 从下标 2 2 2 开始的匹配不可能成功 我们只需要对 T T T 进行预处理。
再比如,当我们进行了第一次匹配后,第四次匹配的前两次比较其实也是多余的,因为我们通过第一次匹配我们得知了 S 4 − 5 = T 4 − 5 S_{4-5}=T_{4-5} S4−5=T4−5 ,而 T 1 − 2 = T 4 − 5 T_{1-2}=T_{4-5} T1−2=T4−5 ,所以我们进行第四次比较时可以直接从 T 3 T_3 T3 开始。
观察一下 T 1 − 4 ≠ T 2 − 5 T_{1-4}\not=T_{2-5} T1−4=T2−5 和 T 1 − 2 = T 4 − 5 T_{1-2}=T_{4-5} T1−2=T4−5 这两个式子有什么共同点?发现一个两边的串一个是 T 1 , k T_{1,k} T1,k 的前缀,另一个是 T 1 , k T_{1,k} T1,k 的后缀,上述两式中 k k k 都等于 5 5 5 (如果看不出来可以多举几个例子)。
(这里的 后缀 也是顺序的,指的是包含最后一个元素的连续一段, 不是逆序的!!! 不要像我一样傻憨憨的以为是倒过来的)
你应该已经有思路了,我们如果能对 T T T 串处理出 所有前缀的最长公共前后缀长度 (比如上例中 T 1 − 5 T_{1-5} T1−5 的最长公共前后缀长度是 2 2 2 ),好像就可以完成上面的两个优化了。
怎么写程序呢?其实不用很多判断,我们维护两个指针 i , j i,j i,j , i i i 指向 S S S 中当前比较到哪里, j j j 指向 S S S 中当前比较的位置的 前一个 。 i i i 指针不断 + 1 +1 +1 就好了,对于 j j j ,我们看看下一个要比较的字符是否与 i i i 的字符相同,相同就 + 1 +1 +1 并比较下去,不同就跳到当前位置的 失配指针 。
等等,什么是失配指针?顾名思义,就是如果在当前位置失配了下一个要比较哪里。那怎么求?这个东西其实就是上文的前缀的最长公共前后缀长度,原因如下:
首先跳过去一定是对的,上文已经解释过了。其次,跳过去中间不会漏下任何情况,看我们最开始的例子,第二次匹配为什么是不必要的?因为 T 1 − 4 T_{1-4} T1−4 一定与 T 2 − 5 T_{2-5} T2−5 不同,如果这两个串相同,就违反了我们 最长 公共前后缀长度的定义,所以不重不漏。
综上,跳到失配指针一定不会有问题。
代码如下:
int nxt[];//失配指针
int kmp(char s[],char t[]) {
get(t);//求失配指针,待会再讲
for(int i=1,j=0;i<=strlen(s+1);i++) {
while(j&&s[i]!=t[j+1]) j=nxt[j];//跳失配指针
if(s[i]==t[j+1]) j++;
if(j==strlen(t+1)) return i-j+1;//找到了返回位置
}
return -1;
}
你可能有疑问,我们在跳失配指针时看起来是有一个内循环的,复杂度正确吗?
事实上当然是正确的,原因在于
j
j
j 每跳一次都会变小,又大于
0
0
0 所以最多跳
j
j
j 次,然而
j
j
j 唯一能变大的地方是 if(s[i]==t[j+1]) j++;
这个语句最多运行
n
n
n 次,因此
j
j
j 最多变大
n
n
n 次,所以跳失配指针也最多跳
n
n
n 次,复杂度正确。
求失配指针
最后我们来考虑求失配指针。
这就很神奇了,结论是:我们 用 T T T 去匹配 T T T 自己 就好了。
代码如下:
void get(char t[]) {
nxt[1]=0;
for(int i=2,j=0;i<=strlen(t+1);i++) {
while(j&&t[i]!=t[j+1]) j=nxt[j];
if(t[i]==t[j+1]) j++;
nxt[i]=j;
}
}
这为什么是对的呢?在我们执行 nxt[i]=j;
语句时,其实就保证了
T
1
−
j
T_{1-j}
T1−j 匹配成功了
T
1
−
i
T_{1-i}
T1−i ,且包含了
T
i
T_i
Ti ,所以
T
1
−
j
T_{1-j}
T1−j 就是
T
1
−
i
T_{1-i}
T1−i 的最长公共前后缀(看不懂再去理解一下匹配
S
S
S 的那个函数)。
完整代码如下:
int nxt[];
void get(char t[]) {
nxt[1]=0;
for(int i=2,j=0;i<=strlen(t+1);i++) {
while(j&&t[i]!=t[j+1]) j=nxt[j];
if(t[i]==t[j+1]) j++;
nxt[i]=j;
}
}
int kmp(char s[],char t[]) {
get(t);
for(int i=1,j=0;i<=strlen(s+1);i++) {
while(j&&s[i]!=t[j+1]) j=nxt[j];
if(s[i]==t[j+1]) j++;
if(j==strlen(t+1)) return i-j+1;
}
return -1;
}