详解 KMP 算法与 next 数组的计算

详解 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 时:
详解 KMP 算法与 next 数组的计算_第1张图片此时 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

详解 KMP 算法与 next 数组的计算_第2张图片
那么,为什么 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,
详解 KMP 算法与 next 数组的计算_第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,代码实现也会略有不同。

你可能感兴趣的:(笔记,数据结构,算法,字符串,c语言,数组)