| | |
|
|
在模式移位之后,朴素(naive)算法就忘记了关于已前匹配的符号的所有信息。所以只能把一个文本符号与不同的模式符号再次做重新比较。这导致了最差的复杂度 (nm) (n: 文本的长度,m: 模式的长度)。 |
|
Knuth、Morris 和 Pratt [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 的前缀 u 或 x 的后缀 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 算法中如何使用字符串的边界的概念来确定移位距离。 | |
|
例子: |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | a | b | c | a | b | c | a | b | d | | | a | b | c | a | b | d | | | | | | | | | a | b | c | a | b | d | | |
|
|
| 在位置 0, ..., 4 的符号已经匹配。在位置 5 比较 c-d 产生一个不匹配。模式可以移位 3 个位置,并在位置 5 恢复比较。 |
|
| 移位距离由匹配前缀 p 的最宽边界确定。在本例中,匹配前缀是 abcab,它的长度是 j = 5。最宽边界是 ab 它的宽度 b = 2。移位距离是 j - b = 5 - 2 = 3。 |
|
|
在预处理阶段,确定模式的每个前缀的最宽边界的宽度。接着在查找阶段,可以依据已经匹配了的前缀来计算移位的距离。 |
|
| | |
|
|
定理: | 设 r、s 是字符串 x 的边界, 这里 |r| < |s|。则 r 是 s 的边界。 |
|
证明: | 图 1 展示了有边界 r 和 s 的字符串 x。因为 r 是 x 的前缀,它也是 s 的真前缀,原因是它比 s 短。但是 r 也是 x 的后缀,因此是 s 的真后缀。故此 r 是 s 的边界。 |
|
|
|
|
|
如果 s 是 x 的最宽边界,则 x 的其次最宽边界 r 是 s 的最宽边界。 |
|
|
定义: | 设 x 是一个字符串且 a A 是一个符号。如果 ra 是 xa 的边界,则 x 的边界 r 可以用 a 来扩展。 |
|
|
|
|
|
图 2 展示了如果 xj = a 则 x 的宽度为 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 : | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | p[j]: | | a | b | a | b | a | a | | b[j]: | | -1 | 0 | 0 | 1 | 2 | 3 | 1 |
|
|
|
| | |
|
|
概念上,上述预处理算法也可以应用到代替 p 的字符串 pt 上。如果只计算直到宽度为 m 的边界,则 pt 的某个前缀 x 的宽度为 m 的边界对应于在 t 中的一次模式匹配(假定边界不是自我重叠的) (图 4)。 |
|
|
|
|
这解释了在预处理算法和下面的查找算法之间的类似性。 |
|
|
查找算法 |
|
|
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 循环。 |
|
|
|
|
如果模式的所有 m 个符号都已经匹配了相应的文本窗口(j = m),调用函数 report 来报告在位置 i-j 上的匹配。然后,移位模式远到它的最宽边界允许的范围。 |
|
在下列例子中展示了查找算法进行的比较,这里绿色表示匹配红色表示不匹配。 |
|
例子: |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | | | a | b | a | b | b | a | b | a | a | | | | | a | b | a | b | a | c | | | | | | | | | | a | b | a | b | a | c | | | | | | | | | | a | b | a | b | a | c | | | | | | | | | a | b | a | b | a | c | | | | | | | | | | a | b | a | b | a | c |
|
|
|
| | |
|
|
预处理算法的内层循环减少 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的文本文件中搜索指定字符串,请问该怎么做)