代码的Big-O复杂度

53

我在算法设计方面有一个关于复杂度的问题。给定了一段代码,我需要计算该代码的复杂度。

伪代码如下:

for(i=1;i<=n;i++){
    j=i
    do{
        k=j;
        j = j / 2;
    }while(k is even);
}

我已经尝试了这个算法,对于一些数字得到了不同的结果。例如,如果n=6,这个算法的输出如下:

i = 1 -> executes 1 time
i = 2 -> executes 2 times
i = 3 -> executes 1 time
i = 4 -> executes 3 times
i = 5 -> executes 1 time
i = 6 -> executes 2 times

它没有一个固定的主题,我应该如何计算它?


5
最坏情况时间复杂度为O(n*log n)。 - sp2danny
1
你的解决方案是什么,伙计?@sp2danny - Behzad Hassani
@sp2danny 我不同意:最坏情况是 n... 请看我的回答或者 @interjay 的! - Samuel Caillerie
5个回答

98
其他答案给出的上限实际上太高了。该算法具有O(n)的运行时间,这比O(n * logn)更紧的上限。
证明:让我们计算一下内部循环将执行多少次。
外部循环运行n次。 内部循环至少运行一次。
对于偶数i,内部循环至少运行两次。 这发生了n/2次。
对于可被4整除的i,内部循环至少运行3次。 这发生了n/4次。
对于可被8整除的i,内部循环至少运行4次。 这发生了n/8次。
...
因此,内部循环运行的总次数为:
n + n/2 + n/4 + n/8 + n/16 + ... <= 2n

内层循环迭代的总次数在n2n之间,即它是Θ(n)。


15
有些惊讶这个回答没有被采纳。虽然技术上来说它是O(n log n),但它并不真正是渐近复杂度...另外,如果你想编辑的话,这里有一个Θ。 - Sabre
4
其他回答所给出的上限实际上太高了。更好的表述是"...实际上比他们需要的要高。" - Taemyr

6
你总是假设每个层级中都出现最坏情况。
现在,你要遍历一个包含N个元素的数组,所以我们从O(N)开始。
现在假设你的i始终等于X,而且X始终为偶数(记住,每次都是最坏情况)。
你需要将X除以2多少次才能得到1?(当偶数除以2达到1时,这是唯一停止除法运算的条件)
换句话说,我们需要解方程 X/2^k = 1 ,其中 X=2^k 并且 k=log<2>(X) 这使得我们的算法需要O(n log<2>(X))步骤,可以轻松写成 O(nlog(n))

5
我最初也做了相同的分析,但是我认为它不完整。虽然外层循环执行了 n 次且内层循环的最坏情况复杂度为 O(log(n)),但并不能得出总体复杂度为 O(n log(n)) 的结论;这取决于内层循环的最坏情况随着 i 从 1 到 n 改变的频率。我猜测这个频率不是 O(n),实际总体复杂度更低(也许是 O(log(n)^2))。 - Ted Hopp
4
但这是一个不公平的要求。该数组具体为 [1, 2, ..., n]。我不认为 O(n log(n)) 是必然的结果。举个极端的例子,假设内部循环在恰好一个值 i 上具有最坏情况下 O(log(i)) 的行为,并且对于所有其他值都是 O(1)。那么外部循环执行 n 次并不会导致总体复杂度为 O(n log(n));它将是 O(n) + O(log(n)) = O(n)。当然,实际的内部循环没有这种极端的行为,但我认为最坏情况密度是否支持 O(n log(n)) 的结论还远未明确。 - Ted Hopp
7
按照这个逻辑,你也可以说复杂度是O(2^n),而且是正确的。但我认为这种推理并不符合问题的精神。 - Ted Hopp
4
不,这不是最坏情况与平均情况的问题。最坏情况仍然是O(n) *(严格来说也意味着它是O(n log n))*。如果n是奇数,它仍然是O(n),因为你需要枚举从1到n的所有值,而不仅仅是n本身。 - BlueRaja - Danny Pflughoeft
4
如果你想成为律师,每个算法都是 O(无穷大) 和 Omega(1),但这并不实际意义。在大多数情况下,Theta界限更加有趣。 - Kevin
显示剩余4条评论

4

对于这样的循环,我们无法分离内部循环和外部循环的计数 -> 变量是紧密相连的!

因此,我们必须计算所有步骤。

实际上,对于每次外部循环(在 i 上),我们会有

1 + v_2(i) steps

其中v_2是2进制估值(例如:参见http://planetmath.org/padicvaluation),它对应于在质因数分解中i的2的幂次方。

因此,如果我们为所有i添加步骤,我们得到的总步数为:

n_steps = \sum_{i=1}^{n} (1 + v_2(i)) 
        = n + v_2(n!)    // since v_2(i) + v_2(j) = v_2(i*j)
        = 2n - s_2(n)    // from Legendre formula (see http://en.wikipedia.org/wiki/Legendre%27s_formula with `p = 2`)

我们接着可以看到,步骤数正好是:
n_steps = 2n - s_2(n)

作为n在二进制中各位数字之和的总和的s_2(n)是可以忽略不计的, 最多只有 log_2(n) 的数量级,因为在二进制中每一位数字都是0或1,而且最多只有log_2(n)个数字。

因此,您的算法复杂度与n等价:

n_steps = O(n)

这个问题的时间复杂度并不是像其他解决方案中所述的O(nlog(n)),而是更小的一个数量级!


提供的示例循环n次,然后在最坏情况下,当i为偶数且是2^x 1 + log_2(i)迭代次数时变为“奇数”,或更准确地说是1 + Omega(n),其中Omega(n)返回质因数的数量,例如6 = 2,3,因此它不是n +... 而是n logn。我认为您错过了第1行的for循环? - Alexander Holman
1
@AlexanderHolman 我不同意:正如此解决方案所述,n_steps = 2*n - s_2(n) 是一个精确的解决方案,而一次迭代的最坏情况可以被扩展,但这仍然是一个最坏的估计而不是一个精确的估计... i 循环的循环总和由以下行指示:n_steps = \sum_{i=1}^n ... - Samuel Caillerie
1
是的,我错了!在这个时间点上,老实说我无法确定你是否正确... 你的魔法似乎起作用了!为你加1分 :) - Alexander Holman

0

让我们从最坏的情况开始:

如果你一直用2(整数)除下去,直到得到1为止,你就不需要停止。基本上,步骤的数量取决于位宽度,这是通过使用二进制对数来发现的。因此内部部分是log n。外部部分显然是n,所以总共是N log N


0

一个do循环将j减半,直到k变成奇数。 k最初是j的副本,ji的副本,因此do运行1 +除以i的2的幂次方:

  • i = 1是奇数,所以它通过do循环一次,
  • i = 2除以2一次,所以是1 + 1,
  • i = 4除以2两次,所以是1 + 2,等等。

这使得最多有1 + log(i)个do执行(以2为底的对数)。

for循环从1到n迭代i,因此上限是n次(1 + log n),即O(n log n)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接