为什么可以在O(n)时间内计算KMP故障函数?

时间:2013-09-07 07:23:31

标签: c++ algorithm knuth-morris-pratt

Wikipedia claims失败函数表可以在O(n)时间内计算。

让我们看看它的“规范”实现(在C ++中):

vector<int> prefix_function (string s) {
    int n = (int) s.length();
    vector<int> pi (n);
    for (int i=1; i<n; ++i) {
        int j = pi[i-1];
        while (j > 0 && s[i] != s[j])
            j = pi[j-1];
        if (s[i] == s[j])  ++j;
        pi[i] = j;
    }
    return pi;
}

为什么它在O(n)时间内工作,即使存在内部while循环?我对算法的分析并不是很强,所以有人可以解释一下吗?

3 个答案:

答案 0 :(得分:8)

这一行:if(s [i] == s [j])++ j;最多执行O(n)次。 它导致p [i]的值增加。请注意,p [i]的起始值与p [i-1]相同。

现在这一行:j = pi [j-1];导致p [i]减少至少一个。并且由于它最多增加了O(n)次(我们的数量也增加和减少了之前的值),因此不能减少超过O(n)次。 所以它也最多执行O(n)次。

因此整个时间复杂度为O(n)。

答案 1 :(得分:4)

这里已经有两个答案是正确的,但我经常认为是完全布局的 证明可以使事情更清楚。你说你想要一个9岁的孩子的答案,但是 我不认为这是可行的(我认为这很容易让人误以为它是真的 没有任何直觉,为什么它是真的)。也许通过这个答案会有所帮助。

首先,外部循环显式运行n次,因为i未被修改 循环内。循环中唯一可以运行多次的代码是 块

while (j > 0 && s[i] != s[j])
{   
    j = pi[j-1]
}   

那么多少次可以运行?请注意,每次这样的条件 我们满意地减少j的值,此时此值最多 pi[i-1]。如果它达到0,那么while循环就完成了。要知道为什么这很重要, 我们首先证明一个引理(你是一个非常聪明的9岁):

pi[i] <= i

这是通过归纳完成的。 pi[0] <= 0因为它在pi的初始化中设置了一次,再也没有触及过。然后归纳我们让0 < k < n并假设 该声明适用于0 <= a < k。考虑p[k]的值。它已经确定了 正好在pi[i] = j行。 j有多大?它被初始化了 归纳为pi[k-1] <= k-1。在while块中,它可以更新为pi[j-1] <= j-1 < pi[k-1]。通过另一个迷你感应,您可以看到j永远不会超过pi[k-1]。因此之后 while循环我们仍有j <= k-1。最后它可能会增加一次,所以我们有 j <= kpi[k] = j <= k(这是我们完成归纳所需证明的内容)。

现在回到原点,我们问“我们可以减少多少次的价值 j“?好了我们的引理,我们现在可以看到while循环的每次迭代都会 单调减少j的值。特别是我们有:

pi[j-1] <= j-1 < j 

那么这次运行多少次?最多pi[i-1]次。精明的读者可能会想到 “你没有证明什么!我们有pi[i-1] <= i-1,但它在while循环中 它仍然是O(n^2)!“。稍微敏锐的读者会注意到这个额外的事实:

  

然而,无论我们多次运行j = pi[j-1],我们都会减少pi[i]的值,从而缩短循环的下一次迭代!

例如,假设j = pi[i-1] = 10。但是在while循环的~6次迭代之后我们有了 j = 3我们假设它在s[i] == s[j]行中增加1,因此j = 4 = pi[i]。 那么在外循环的下一次迭代中,我们从j = 4开始......所以我们最多只能执行while 4次。

最后一个难题是每个循环++j最多运行一次。所以它不像我们可以拥有的那样 我们的pi向量中包含类似的内容:

0 1 2 3 4 5 1 6 1 7 1 8 1 9 1
           ^   ^   ^   ^   ^
Those spots might mean multiple iterations of the while loop if this 
could happen

为了使其真正正式,您可以建立上述不变量,然后使用归纳法 要显示运行while循环的次,与pi[i]求和的最多为i。 由此可见,{em>总运行while循环的次数为O(n),这意味着整个外循环具有复杂性:

O(n)     // from the rest of the outer loop excluding the while loop
+ O(n)   // from the while loop
=> O(n) 

答案 2 :(得分:3)

让我们从外循环执行n次开始,其中n是我们寻找的模式的长度。由于j,内循环将pi[j] < j的值减少至少1。循环最晚在j == -1时终止,因此它最多可以减少j的值,因为j++(外部循环)之前已经增加了j++。由于n在外循环中执行的时间恰好n次,因此内部while循环的总执行次数限制为/* ff stands for 'failure function': */ void kmp_table(const char *needle, int *ff, size_t nff) { int pos = 2, cnd = 0; if (nff > 1){ ff[0] = -1; ff[1] = 0; } else { ff[0] = -1; } while (pos < nff) { if (needle[pos - 1] == needle[cnd]) { ff[pos++] = ++cnd; } else if (cnd > 0) { cnd = ff[cnd]; /* This is O(1) for the reasons above. */ } else { ff[pos++] = 0; } } } 。因此,预处理算法需要O(n)步。

如果您关心,请考虑预处理阶段的这种更简单的实现:

{{1}}

很明显失败函数是O(n),其中n是所寻求模式的长度。