KMP算法是由D.E.Knuth、V.R.Pratt 和 J.H.Morris 三个牛提出来的,因此得名。
【Cook于1970年证明的一个理论得到,任何一个可以使用被称为下推自动机的计算机抽象模型来解决的问题,也可以使用一个实际的计算机(更精确的说,使用一个随机存取机)在与问题规模对应的时间内解决。特别地,这个理论暗示存在着一个算法可以在大约m+n的时间内解决模式匹配问题,这里m和n分别是存储文本和模式串数组的最大索引。Knuth 和Pratt努力地重建了 Cook的证明,由此创建了这个模式匹配算法。大概是同一时间,Morris在考虑设计一个文本编辑器的实际问题的过程中创建了差不多是同样的算法。这里可以看到并不是所有的算法都是“灵光一现”中被发现的,而理论化的计算机科学确实在一些时候会应用到实际的应用中。】
字符串模式匹配算法,即回答,B串是否是A串的子串(A串是否包含B串)。KMP即为其中一种快速算法。
简单匹配算法,即穷举法的最坏时间复杂度为O(m*n); KMP匹配算法,可以证明它的时间复杂度为O(m+n),其中匹配扫描的最坏时间复杂度为O(m),预处理则为O(n)。
MP/KMP 字符串搜索算法的思想精华在于利用已经匹配的部分包含的信息,加速搜索的过程。
形象地理解KMP:扫描字符串A,并更新可以匹配到B的什么位置。
先假设并在下面的过程中始终保证:A[i-j+ 1..i]与B[1..j]完全匹配,i和j分别表示两个指针。
然后随着 i 不断增加,j 相应地变化。接下来比较A[i+1]和B[j+1]。
当A[i+1]=B[j+1]时,i 和 j 各加一;若此时j=m,则B是A的子串,此时 i 值即为匹配位置。
当A[i+1]<>B[j+1],KMP的策略是适当减小 j 值(等于P[j])使得A[i-j+1..i]与B[1..j]重新保持匹配且新的B[j+1]恰好与A[i+1]匹配,从而使得i和j能继续增加。
这里借助预处理的数组P[j],用来记录所有满足B[1..P[j]]=B[j-P[j]+1..j]的最大值,可以描述为 "不为自身的最大首尾重复子串长度"。
1 j:=0; 2 for i:=1 to n do 3 begin 4 while (j>0) and (B[j+1]<>A[i]) do j:=P[j]; 5 if B[j+1]=A[i] then j:=j+1; 6 if j=m then 7 begin 8 writeln('Pattern occurs with shift ',i-m); 9 j:=P[j]; 10 end; 11 end;
预处理环节:很多方法都巧妙地运用了预处理,使得在线性的时间里解决字符串的匹配。
其实,这里的预处理本质是B串进行自我匹配的一个KMP算法,即将B串看作主串,B串的一部分看做子串,自己匹配自己。因此,预处理计算P[j]数组的代码与KMP扫描代码相似度很高。
1 P[1]:=0; 2 j:=0; 3 for i:=2 to m do 4 begin 5 while (j>0) and (B[j+1]<>B[i]) do j:=P[j]; 6 if B[j+1]=B[i] then j:=j+1; 7 P[i]:=j; 8 end;
KMP的时间复杂度分析:摊还分析法,即通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。这里主要针对while循环使用。
参考资料: