KMP 算法
Posted lpf-666
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMP 算法相关的知识,希望对你有一定的参考价值。
简述
KMP 算法,又称模式匹配算法,能够在线性时间内判定字符串 (A[1-N]) 是否为字符串 (B[1-M]) 的子串。
对于刚刚接触 KMP 的同学来说,理解起来比较困难,难以理解 (next[]) 数组的实际意义。
当然你要硬背 KMP 也没人拦着你,因为代码确实就十几行
但是其实并没有那么难懂,产生畏惧情绪更是不必要的,现在谈谈 KMP。
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”。
这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。
主要的难点和中心点就是 (next[]) 数组和 (f[]) 数组的求解,其中又以 (next[]) 为重。
下文将展开详细的叙述。
暴力算法与整体框架
暴力
首先,暴力很好想,就是枚举比较,时间 (O(NM)) 。 (这个应该都会吧)
但是,谁都会发现这种算法的效率很低,但原因是什么,请看下图。
其中 (txt) 是原字符串,(pat) 是子串。
很明显,pat
中根本没有字符 c,根本没必要回退指针 i
,暴力解法明显多做了很多不必要的操作。
KMP 框架
KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明:
再比如类似的 (txt = "aaaaaaab") (pat = "aaab")。
暴力解法还会和上面那个例子一样蠢蠢地回退指针 i
,而 KMP 算法又会耍聪明:
综上,KMP 快速的原因是:避免了不必要的多余匹配。
而且,KMP 算法不仅能更高效、更准确的处理这个问题,还可以提供一些额外的信息。
具体的讲,KMP 算法共分为两步:
- 对字符串 (A) 进行自我匹配,求出一个 (next[]) ,其中 (test[i]) 表示 "(A) 中以 (i) 结尾的非前缀子串" 与 "(A) 的前缀"的最大匹配长度(即最大前缀后缀),即:(next[i]=max){(j)},其中 (j<i) 并且 (A[i-j+1)~(i]=A[1-j])。
- 对字符串 (A) 与 (B) 进行匹配,求出一个数组 (f),其中 (f[i]) 示 "(B) 中以 (i) 结尾的非前缀子串" 与 "(A) 的前缀"的最大匹配长度,即:(f[i]=max){(j)},其中 (j<i) 并且 (B[i-j+1)~(i]=A[1-j])。
是不是发现两者很相像(所以代码也很像),下面我们将详细讲解 (next[]) 数组的求解方法。
next 数组
最长前缀后缀
可能很多同学看到上面两个步骤就昏倒,其实也没有那么难,通俗的讲 (next) 就是最长前缀后缀(好像没有很通俗)
如果给定的模式串 (A=“ABCDABD”),从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):
引理
根据这个表可以得出下述结论:
失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
记住这是很重要的一个结论,证明比较繁琐,可以自行百度,但我是直接记住的。(只在记不住就靠数学能力吧)
这只是通俗的讲,真正的引理是:
若 (j_0) 是 (next[i]) 的“候选项”,即 (j_0<i) 且 (A[i-j_0+1)~(i]) = (A[1)~(i]) ,则小于 (j_0) 的最大的 (next[i]) 的“候选项”是 (next[j_0]) 。换句话说,(next[j_0]+1)~(j_0-1) 之间的数都不是 (next[i]) 的“候选项”。
请读者务必确定理解这条引理之后再往下看,实在不行可以借助百度以及其他资料。
根据引理求解 next 数组
简单说就是根据递归求法求解答案。
假设已求出 (next[i-1]) 的值,则 (next[i-1]) 的所有候选项为 (next[i-1]),(next[next[i-1]]),(next[next[next[i-1]]])等。
(j) 是 (next[i]) 的候选项的前提是 (j-1) 是 (next[i-1]) 的候选项。
((A[i-j+1)~(i]) = (A[1)~(j]) 的前提是 (A[i-j+1)~(i-1]) = (A[1)~(j-1]))
因此在计算 (next[i]) 是只需要把 (next[i-1]+1),(next[next[i-1]]+1),(next[next[next[i-1]]]+1)等作为候选项即可。
具体看代码:
- 初始化 (next[i]=j=0),假设 (next[i-1]) 已经求出,下面求解 (next[i])。
- 不断尝试扩张 (j) ,如果扩张失败令 (j=next[j]) ,直至 (j=0)。
- 如果能够扩展成功(下一个字符相等),匹配长度 (j+1) ,(next[i]=j)。
next[1]=0;
for(int i=2,j=0;i<=n;i++){
while(j>0 && a[i]!=a[j+1]) j=next[j];
if(a[i]==a[j+1]) j++;
next[i]=j;
}
f 数组求解
求解
因为定义的相似性,两者代码几乎一样。
for(int i=1,j=0;i<=m;i++){
while(j>0 &&(j==n || b[i]!=a[j+1])) j=next[j];
if(b[i]==a[j+1]) j++;
f[i]=j;
//if(f[i]==n) 此时就是 A 在 B 中的某次出现
}
模板题
就是模板题而已。
下面给出完整代码:
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#define maxn 1000010
using namespace std;
int l1,l2,next[maxn],f[maxn];
char s1[maxn],s2[maxn];
int main(){
cin>>s1+1;
cin>>s2+1;
l1=strlen(s1+1);
l2=strlen(s2+1);
next[1]=0;
for(int i=2,j=0;i<=l2;i++){
while(j>0 && s2[i]!=s2[j+1]){
j=next[j];
}
if(s2[j+1]==s2[i]) j++;
next[i]=j;
}
for(int i=1,j=0;i<=l1;i++){
while(j>0 &&(j==l1 || s1[i]!=s2[j+1])){
j=next[j];
}
if(s1[i]==s2[j+1]) j++;
f[i]=j;
}
for(int i=1;i<=l1;i++)
if(f[i]==l2) printf("%d
",i-l2+1);
for(int i=1;i<=l2;i++)
printf("%d ",next[i]);
return 0;
}
总结
KMP 算法到这里就结束了,这篇文章可能对初学者不太友好,不过也可以加深理解吧。
其时间复杂度为:(O(N+M)) 。
当然你,其复杂度是可以优化的,而且也有更优的算法,时候回继续 (Update)。
注:
全篇完。
以上是关于KMP 算法的主要内容,如果未能解决你的问题,请参考以下文章