详解 KMP 算法与 next 数组的计算
1. KMP 算法
KMP 算法为 BF 算法的优化,当模式串与主串之间存在许多 “部分匹配” 的情况下比 BF 算法快很多。其核心是利用匹配失败后的信息,具体通过一个 next 数组实现,数组本身包含了模式串的局部匹配信息。
下面给出 KMP 与 BF 算法的代码实现:
// KMP 算法的实现
int KMP(string S, string T, next)
{
int i, j;
i = j = 0;
while (i < S.length && j < T.length)
{
if (j == -1 || S[i] == T[j]) { ++i; ++j; }
else j = next[j];
}
if (j == T.length) return i - T.length;
else return 0;
} // 时间复杂度:O(m+n)
// BF 算法的实现
int BF(string S, string T)
{
int i, j;
i = j = 0;
while (i < S.length && j < T.length)
{
if (S[i] == T[j]) { ++i; ++j; }
else { i = i - j + 1; j = 0; }
}
if (j == T.length) return i = T.length;
else return 0;
} // 时间复杂度:O(mn)
横向对比两种算法,我们发现其区别仅仅在于第一个 if…else 语句不同,KMP 中 i 不需要回溯,j 仅仅是回溯的一小段距离 ( 之后的 next 数组中会解释 )。这便使得这两种算法的时间效率有着很大的差距。那么 KMP 中 j 是如何回溯的呢?
假设我们模式串 T = abaabc,主串 S = acabaabaabc,那么当 i = 8,j = 6 时:
此时 T[6] 与 S[8] 失配,如果我们可以通过仅仅回溯 j ( 或者说将 j 向前滑动,因为 i 不变 ) 就使 T[6] 与 S[8] 继续匹配的话,显然就能省下大量的时间。而在 KMP 中,这里 j 会回溯 ( 或滑动 ) 至 next[6] = 2 即模式 T 中的第 3 个元素,以使得子串与模式串继续匹配。
KMP 中的代码实现:
j = next[j] // next[j] = 3
那么,为什么 j 应该回溯 ( 或滑动 ) 至第 3 位呢?这里就涉及到了 next 数组的计算。
2. next 数组的计算
这里就直接上代码了:
void next(string T, int next[])
{
i = 0; next[0] = -1; j = 0;
while (i < T[0])
{
if (j == -1 || T[i] == T[j]) { ++i; ++j; next[i] = j; }
else j = next[j];
}
}
这段代码短小精悍,但其含义就是不断地寻找模式串 T 中的最长相同前缀后缀。
比方说串 abcdabc,其前缀就有:
{ a,ab,abc,abcd,abcda,abcdab },
后缀有:
{ c,bc,abc,dabc,cdabc,bcdabc },
二者的最长相同前缀后缀为 abc,其长度为 3,因此 next[7] = 3。
虽然最后一个 c 的索引为 6,但是我们计算的永远是下一位失配时的 next 值,所以应对应 c 的下一位,即索引为 7 的位置。
同时,我们规定 next[0] = -1,当 next 数组回溯到 next[0] 时,j = next[0] = -1,此时从主串 S 的下一个位置开始比较。
++i, ++j;
依次计算接下来的 next 值:
next[1] ( a,无相同前缀后缀 ) = 0,
next[2] ( ab ) = 0,
next[3] ( abc ) = 0,
next[4] ( abcd ) = 0,
next[5] ( abcda ) = 1,
next[6] ( abcdab ) = 2,
next[7] ( abcdabc ) = 3。
next[0] | next[1] | next[2] | next[3] | next[4] | next[5] | next[6] | next[7] |
---|---|---|---|---|---|---|---|
-1 | 0 | 0 | 0 | 0 | 1 | 2 | 3 |
寻找最长相同前缀后缀的作用在于:比方说主串 S 与模式 T 在第 8 位失配时:j = next[7] = 3,
此时利用 next 数组便使得 abc 这三个字符不用再次比较。
3. nextval 数组的计算
nextval 数组为 next 数组的修正值,比方说对于模式 T:aaaab,其 next 数组为:
next[0] | next[1] | next[2] | next[3] | next[4] |
---|---|---|---|---|
-1 | 0 | 1 | 2 | 3 |
如果 T[3] 不匹配了 T[2] = T[3] 肯定也不匹配,T[1] = T[2] 也不匹配,故不需回溯到 next[2],next[1],可以直接让其回溯到 next[0],故新的 nextval 数组为:
next[0] | next[1] | next[2] | next[3] | next[4] |
---|---|---|---|---|
-1 | -1 | -1 | -1 | 3 |
代码实现如下:
void nextval(string T, int nextval[])
{
i = 0; nextval[1] = -1; j = 0;
while (i < T[0])
{
if (j == -1 || T[i] == T[j])
{
++i; ++j;
if (T[i] != T[j])
nextval[i] = j;
else nextval[i] = nextval[j];
}
else j = nextval[j];
}
}
4. 注意事项
本文中的串并未经类型定义,因而其索引从 0 开始,而通常经过类型定义的串索引从 1 开始,此时应将 next 与 nextval 数组的索引与值各自加 1,代码实现也会略有不同。