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循环?我对算法的分析并不是很强,所以有人可以解释一下吗?
答案 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 <= k
等pi[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是所寻求模式的长度。