【问题标题】:Run time of a function函数的运行时间
【发布时间】:2016-02-13 18:32:50
【问题描述】:

设函数 f() 为:

void f(int n)
{
   for (int i=1; i<=n; i++)
     for (int j=1; j<=n*n/i; j+=i)
       printf(“*”);
}

根据我的计算,Big O 方法的运行时间应该是 O(n2log n)。
答案是 O(n2)。这是为什么呢?

【问题讨论】:

  • @Baronz 这看起来像是一组完全不同的循环,具有非常不同的分析。我错过了什么吗?
  • 大 O 表示法是关于尺寸接近大值时的限制。你可以看一眼这个函数的“形状”,它是一个嵌套的 for 循环。 Big O 只取最高值,没有常数,所以 Dina 的循环减少到与 Simucal 提出的重复问题相同的形状
  • @baronz:渐近行为不仅仅是嵌套循环数的函数。
  • 当 n 变得非常大时,log n 不是很简单吗? n^2 是驱动复杂度。 en.wikipedia.org/wiki/Big_O_notation

标签: c runtime big-o time-complexity


【解决方案1】:

我欠你一个道歉。我第一次看错了你的代码,所以我给出的最初答案是不正确的。这是一个更正的答案,以及与解释我的分析出错的原始答案的比较。我希望你觉得这很有趣 - 我认为这会产生一些非常酷的数学!

您发布的代码如下所示:

for (int i=1; i<=n; i++)
  for (int j=1; j<=n*n/i; j+=i)
     printf(“*”);

要确定这段代码的运行时间,让我们看看内循环在所有迭代中做了多少工作。当 i = 1 时,循环计数到 n2 个,所以它确实 n2 工作。当 i = 2 时,循环计数最多为 n2 / 2,因此它确实 n2 / 4 工作。当 i = 3 时,循环计数到 n2 / 3 三个,所以它确实 n2 / 9 工作。更一般地,第 k 次迭代执行 n2 / k2 工作,因为它以大小为 k 的步长计数到 n2 / k。

如果我们总结这里完成的工作 i 范围从 1 到 n,包括在内,我们看到运行时是

n2 + n2 / 4 + n2 / 9 + n2 / 16 + 。 .. + n2 / n2

= n2 (1 + 1/4 + 1/9 + 1/16 + 1/25 + ... + 1/n2)。

这里的求和 (1 + 1/4 + 1/9 + 1/16 + ...) 具有(令人惊讶的!)属性,在极限情况下,it's exactly equal to π2 / 6. 换句话说,您的代码的运行时渐近接近 n2 π / 6,因此运行时间为 O(n2)。您可以通过编写一个将实际步数与 n2 π / 6 进行比较并查看结果的程序来了解这一点。

我第一次弄错了,因为我误读了你的代码,好像它是这样写的

for (int i=1; i<=n; i++)
  for (int j=1; j<=n*n/i; j+=1)
     printf(“*”);

换句话说,我认为内部循环在每次迭代中采用大小为 1 的步数,而不是大小为 i 的步数。在这种情况下,循环的第 k 次迭代所做的工作是 n2 / k,而不是 n2 / k2,即运行时间为

n2 + n2/2 + n2/3 + n2/4 + 。 ..n2/n

= n2(1 + 1/2 + 1/3 + 1/4 + ... + 1/n)

在这里,我们可以使用 1 + 1/2 + 1/3 + ... + 1/n 是众所周知的求和。第 n 个harmonic number 定义为 Hn = 1 + 1/2 + 1/3 + ... + 1/n 已知调和数服从 Hn sub> = Θ(log n),所以这个版本的代码运行时间为 O(n2 log n)。有趣的是,这种变化如何如此显着地改变了代码的运行时间!

作为一个有趣的概括,假设您更改了内部循环,使得对于某些 ε > 0 的步长为 iε(并假设您向上取整)。在这种情况下,通过内循环的第 k 次迭代次数将为 n2 / k1 + ε,因为循环的上限为 n2 / k 并且您正在采取大小为 kε 的步数。通过与我们之前看到的类似的分析,运行时将是

n2 + n2 / 21+ε + n2 / 31 +ε + n2 / 31+ε + ... + n2 / n1+ε

= n2(1 + 1/21+ε + 1/31+ε + 1/4 1+ε + ... + 1/n1+ε)

如果您参加过微积分课程,您可能会认识到该系列

1 + 1/21+ε + 1/31+ε + 1/41+ε + ... + 1/n1+ε

收敛到任何 ε > 0 的某个固定限制,这意味着如果步长是 any i 的正幂,则整体运行时间将为 O(n2 )。这意味着以下所有代码段的运行时间都为 O(n2):

for (int i=1; i<=n; i++)
  for (int j=1; j<=n*n/i; j+=i)
     printf(“*”);

for (int i=1; i<=n; i++)
  for (int j=1; j<=n*n/i; j+=i*i)
     printf(“*”);

for (int i=1; i<=n; i++)
  for (int j=1; j<=n*n/i; j+=i*(sqrt(i) + 1))
     printf(“*”);

【讨论】:

    【解决方案2】:

    第一个循环的运行时间是n,第二个循环的运行时间是(n/i)^2(不是n^2/i),因为我们有j+=i(不是j++)。所以总时间如下:

    ∑{i=1to n}(n/i)^2 = n^2∑{i=1to n}(1/i)^2 &lt; 2*n^2

    所以时间复杂度是O(n^2)

    【讨论】:

    • 感谢您指出我在原始答案中犯的错误。我已将其替换为一个新的答案,详细说明了分析并与原始答案进行了比较。
    【解决方案3】:

    从我从理论中了解到,i 不会对复杂性产生太大影响。由于您有指数函数,因此 log n 将被忽略。因此,它只会被认为是大 O(n2) 而不是预期的 O(n2log n)。

    回想一下,当我们使用大 O 表示法时,我们会删除常量和低阶项。这是因为当问题变得足够大时,这些术语就无关紧要了。然而,这意味着两种算法可以具有相同的大 O 时间复杂度,即使一个总是比另一个快。例如,假设算法 1 需要 N2 时间,算法 2 需要 10 * N2 + N 时间。对于这两种算法,时间都是 O(N2),但是算法 1 总是比算法 2 快。在这种情况下,常数和低阶项对于哪个算法是很重要的实际上更快。

    但是,重要的是要注意,就算法如何“扩展”的问题(即当问题规模翻倍时算法的时间如何变化)而言,常数并不重要。虽然需要 N2 时间的算法总是比需要 10*N2 时间的算法快,但对于这两种算法,如果问题规模翻倍,实际时间将翻两番。

    当两种算法具有不同的大 O 时间复杂度时,常数和低阶项仅在问题规模较小时才有意义。例如,即使涉及到很大的常数,线性时间算法最终也总是比二次时间算法快。这在下表中进行了说明,该表显示了 100*N 的值(N 的线性时间)和 N2/100 的值(N 的二次时间) N 的一些值。对于小于 104 的 N 值,二次时间小于线性时间。但是,对于所有大于 104 的 N 值,线性时间都更小。

    查看this文章了解更多详情。

    【讨论】:

    • 你有 n^2 log n。在这里,log n 只是一个常数,它在您为其赋值的那一刻被定义。是的,我确定它是 O(n^2)。
    • 那你为什么不从 O(nlogn) 中删除 log n 呢?
    • 你不能像这样从 big-O 中删除日志项。那样不行。
    • @templatetypedef,我知道你的意思,但由于它是时间复杂度,你不要保留常量。这完全取决于程序的扩展方式。
    • @SimplyMe 你说O(log n) 永远不会超过64,然后声称log(10^100) 小于500。如果你的陈述是真的,它不会小于64 ?我想大家都同意O(n^2 log n)O(n^2)在实践中大同小异,只是函数的顺序不一样。也就是说,没有Kx,所以n^2 log n &lt; K * n^2代表所有n &gt; x。对于我们选择的任何K,对于足够大的n,我们总是有log n &gt; K。因此我们不能放弃log n 因素。
    猜你喜欢
    • 2023-04-02
    • 1970-01-01
    • 2015-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多