KMP算法

来源:互联网 发布:开机修改分辨率软件 编辑:程序博客网 时间:2024/05/01 03:37

Knuth-Morris-Pratt 算法

想法

在模式移位之后,朴素(naive)算法就忘记了关于已前匹配的符号的所有信息。所以只能把一个文本符号与不同的模式符号再次做重新比较。这导致了最差的复杂度 (nm) (n: 文本的长度,m: 模式的长度)。 Knuth、MorrisPratt [KMP] 算法利用了从以前的符号比较获得的信息。它永不重新比较已经与一个模式符号匹配了的一个文本符号。结果是,Knuth-Morris-Pratt 算法在查找阶段的复杂度是 O(n)。 但是,需要一次预处理来分析模式的结构。预处理阶段的复杂度是 O(m)。因为 m n,Knuth-Morris-Pratt 算法的整体的复杂度是 O(n)。

基本定义

定义:设 A 为字母表, x = x0 ... xk-1,是在 A 上的长度为 k 的一个字符串。 x前缀(prefix)是一个子串 u u  =  x0 ... xb-1  这里  b  {0, ..., k} 就是说 x 开始于 u。 x后缀(suffix)是一个子串 u u  =  xk-b ... xk-1 这里  b  {0, ..., k} 就是说 x 结束于 u。 如果 ux,也就是说它的长度 b 小于 k,则  x 的前缀 ux 的后缀 u 分别的叫做真(proper)前缀真后缀x边界(border)是一个子串 r r  =  x0 ... xb-1  且  r  =  xk-b ... xk-1  这里  b  {0, ..., k-1} x 的边界是既是 x 的真前缀又是真后缀的子串。我们称它的长度 b 为边界的宽度。 例子:x = abacab。x 的真前缀是  , a, ab, aba, abac, abaca x 的真后缀是  , b, ab, cab, acab, bacab x 的边界是  , ab 边界 的宽度是 0,边界 ab 的宽度是 2。 对于所有的 x  A+,空串 总是 x 的边界。空串 自身没有边界。 下列例子演示了在 Knuth-Morris-Pratt 算法中如何使用字符串的边界的概念来确定移位距离。  例子:0123456789...abcabcabd abcabdabcabd在位置 0, ..., 4 的符号已经匹配。在位置 5 比较 c-d 产生一个不匹配。模式可以移位 3 个位置,并在位置 5 恢复比较。 移位距离由匹配前缀 p 的最宽边界确定。在本例中,匹配前缀是 abcab,它的长度是 j = 5。最宽边界是 ab 它的宽度 b = 2。移位距离是 j  - b  =  5 - 2  =  3。 在预处理阶段,确定模式的每个前缀的最宽边界的宽度。接着在查找阶段,可以依据已经匹配了的前缀来计算移位的距离。

预处理

定理:rs 是字符串 x 的边界, 这里 |r| < |s|。则 rs 的边界。证明:图 1 展示了有边界 rs 的字符串 x。因为 rx 的前缀,它也是 s 的真前缀,原因是它比 s 短。但是 r 也是 x 的后缀,因此是 s 的真后缀。故此 rs 的边界。图 1:  字符串 x 的边界 r、s如果 sx 的最宽边界,则 x 的其次最宽边界 rs 的最宽边界。 定义:x 是一个字符串且 a  A 是一个符号。如果 raxa 的边界,则 x 的边界 r 可以用 a 来扩展。图 2:  边界的扩展图 2 展示了如果 xj = ax 的宽度为 j 的边界可以用 a 来扩展。 在预处理阶段可以计算一个长度为 m +1 的数组 b。每个条目 b[i] 包含这个模式的长度为 i (i = 0, ..., m)的前缀的最宽边界的宽度。因为长度为 i = 0 的前缀 没有边界,我们设置 b[0] = -1。 图 3:  模式的长度为 i 并带有宽度为 b[i] 的边界的前缀。假如 b[0], ..., b[i] 的值已知,则通过检查前缀 p0 ... pi-1 是否可以用符号 pi 来扩展,就可以计算 b[i+1] 的值。这种情况下是检查 pb[i] = pi (图 3)。要检查的边界可以按递减次序从 b[i], b[b[i]] 等中获得。 预处理算法由带有采用这些值的变量 j 的一个循环构成。如果 pj = pi,则宽度为 j 的边界可以用 pi,来扩展。否则,通过设置 j = b[j] 来检查其次最宽边界。如果最后没有边界(j = -1)可以扩展则循环终止。 在每次用语句 j++ 增加 j 之后,j 就是 p0 ... pi 的最宽边界的宽度。把这个值写到 b[i+1](到用语句 i++ 增加 i 之后的 b[i])。 预处理算法
void kmpPreprocess(){    int i=0, j=-1;    b[i]=j;    while (i<m) {        while (j>=0 && p[i]!=p[j])            j=b[j];        i++;        j++;        b[i]=j;    }}
例子:对于模式 p = ababaa 在数组 b 中的边界宽度是如下值。例如这里 b[5] = 3,因为长度为 5 的前缀 ababa 有宽度为 3 的边界。

   j :

 0123456 p[j]: ababaa b[j]: -1001231

查找算法

概念上,上述预处理算法也可以应用到代替 p 的字符串 pt 上。如果只计算直到宽度为 m 的边界,则 pt 的某个前缀 x 的宽度为 m 的边界对应于在 t 中的一次模式匹配(假定边界不是自我重叠的) (图 4)。 4:   pt 的前缀 x 的宽度为 m 的边界这解释了在预处理算法和下面的查找算法之间的类似性。 查找算法
void kmpSearch(){    int i=0, j=0;    while (i<n) {        while (j>=0 && t[i]!=p[j])            j=b[j];        i++;        j++;        if (j==m) {            report(i-j);            j=b[j];        }    }}
当在最内层 while 循环中在位置 j 发生了一次不匹配的时候,考虑模式的长度为 j 的匹配前缀的最宽边界(图 5)。在位置 b[j] 恢复比较,边界的宽度产生导致边界匹配的一次模式移位。如果不匹配再次发生,考虑其次最宽边界。以次类推,直到没有边界剩下(j = -1)或下一个符号匹配。接着我们有了模式的一个新的匹配前缀并继续外层 while 循环。 图 5:  当在位置j 发生不匹配的时候模式的移位如果模式的所有 m 个符号都已经匹配了相应的文本窗口(j = m),调用函数 report 来报告在位置 i-j 上的匹配。然后,移位模式远到它的最宽边界允许的范围。 在下列例子中展示了查找算法进行的比较,这里绿色表示匹配红色表示不匹配。 例子:0123456789...ababbabaa ababacababacababacababacababac

分析

预处理算法的内层循环减少 j 的值至少为 1,因为 b[j] < j。循环最迟在 j = -1 的时候终止,所以它能减少 j 的值至多同以前的 j++ 增加它一样的次数。因为 j++ 在外层循环精确的执行 m 次,内层 while 循环的全部的执行次数限制为 m。所以预处理算法需要 O(m) 个步骤。 同理查找算法需要 O(n) 步骤。上面的例子展示了这些步骤: 这些比较(绿色和红色符号)形成了"楼梯"。整个楼梯最多能同它的高度一样宽,所以最多进行 2n 次比较。 因为 m n 所以 Knuth-Morris-Pratt 算法的整体复杂度是 O(n)。

引用

[KMP]D.E. Knuth, J.H. Morris, V.R. Pratt: Fast Pattern Matching in Strings. SIAM Journal of Computing 6, 2, 323-350 (1977)

原文链接:http://mhss.nease.net/string/KMP.html

CSDN相关贴子:http://community.csdn.net/Expert/topic/4409/4409271.xml?temp=.4238092 (我想在一个10M的文本文件中搜索指定字符串,请问该怎么做)

原创粉丝点击