【问题标题】:What is the time complexity \big(O) of this specific function?这个特定函数的时间复杂度 \big(O) 是多少?
【发布时间】:2018-10-05 22:47:14
【问题描述】:

这个函数(f1)的时间复杂度是多少?

我可以看到第一个循环(i=0)->(n/4 次)第二个(i=3)->(n/4 - 3 次)....等等,结果是:(n/3)*(n/4 + (n-3)/4 + (n-6)/4 + (n-9)/4 ....

我停在这里,如何继续?

int f1(int n){
  int s=0;
  for(int i=0; i<n; i+=3)
    for (int j=n; j>i; j-=4)
      s+=j+i;
  return s;
}

【问题讨论】:

  • 不是 O(n^2) 吗?如果 n 翻倍,则执行时间将翻两番。这类似于比较每个数组元素for (i=0;i&lt;n;++i) for (j=0;j&lt;i;++j),所以我猜它是 O(n^2)。常量对于时间复杂度并不重要

标签: c loops time-complexity big-o complexity-theory


【解决方案1】:

Big(O) 表示法的重要之处在于它消除了“常量”。目标是随着输入大小的增长确定趋势,而不考虑具体数字。

将其视为在您不知道 x 轴和 y 轴的数字范围的图形上确定曲线。

因此,在您的代码中,即使您为每个循环的每次迭代都跳过了n 范围内的大部分值,但这是以恒定速率完成的。因此,无论您实际跳过多少,这仍然相对于 n^2 进行缩放。

您是否计算以下任何一项都没有关系:

1/4 * n^2
0.0000001 * n^2
(1/4 * n)^2
(0.0000001 * n)^2
1000000 + n^2
n^2 + 10000000 * n

在 Big O 中,这些都等价于O(n^2)。关键是一旦n 变得足够大足够(无论是什么),所有低阶项和常数因子在“大图”中变得无关紧要。

值得强调的是,这就是为什么在小投入上你应该警惕过度依赖大 O。那时持续的开销仍然会产生很大的影响。

【讨论】:

  • @sam0101 正如其他人所说,对循环的简单检查表明复杂性是 N 的分数乘以 N 的分数:an * bn = abn^2 ==&gt; O(n^2)。但是如果你正在寻找一个正式的证明,我认为这是一个家庭作业。如果是这样,我建议您检查您的笔记以获取选择排序的示例证明。它将遵循相同的原则。
  • 所以如果它 n^2 - n^/4 ... 它仍然是 O(n^2)?
  • @sam0101 是的n^2 - (n^2)/4 可以写成(1 - 1/4)* n^2¾ n²,3/4 只是一个常数系数。
【解决方案2】:

关键观察:内部循环在步骤i中执行(n-i)/4次,因此在步骤n-i中执行i/4

现在将i = 3k, 3(k-1), 3(k-2), ..., 9, 6, 3, 0 的所有这些数量相加,其中3k3n 之前的最大倍数(即3k &lt;= n &lt; 3(k+1)):

3k/4 + 3(k-1)/4 + ... + 6/4 + 3/4 + 0/4 = 3/4(k + (k-1) + ... + 2 + 1)
                                        = 3/4(k(k+1))/2
                                        = O(k^2)
                                        = O(n^2)

因为k &lt;= n/3 &lt;= k+1,因此k^2 &lt;= n^2/9 &lt;= (k+1)^2 &lt;= 4k^2

【讨论】:

    【解决方案3】:

    理论上是“O(n*n)”,但是...

    如果编译器想把它优化成这样怎么办:

    int f1(int n){
      int s=0;
      for(int i=0; i<n; i+=3)
        s += table[i];
      return s;
    }
    

    甚至这个:

    int f1(int n){
      if(n <= 0) return 0;
      return table[n];
    }
    

    那么它也可以是“O(n)”或“O(1)”。

    请注意,从表面上看,这些优化似乎不切实际(由于最坏情况下的内存成本);但是使用足够先进的编译器(例如,使用“整个程序优化”来检查所有调用者并确定n 始终在某个范围内)并非不可想象。以类似的方式,所有调用者都使用常量并非不可能(例如,足够先进的编译器可以将 x = f1(123); 替换为 x = constant_calculated_at_compile_time)。

    换句话说;实际上,原始函数的时间复杂度取决于函数的使用方式以及编译器的好坏。

    【讨论】:

    • 显然选择排序可以随时替换为合并/快速/堆/其他排序。这并不意味着选择排序变成O(n.log(n))。注意:实际代码的复杂性不会改变,因为编译器会选择应用一些特定的优化。如果编译后的代码经过优化和转置以执行不同的操作(但可能是等效的),则不同的代码具有不同的复杂性。如果编译器要在编译时调用该函数以将二进制文件转换为 const 查找,那么复杂性 O(n^2) 仍然发生在编译时
    • @Watcher:理论上你是对的;但是对于这两个例子(你的和我的),我们仍然达到了“理论上,理论是有用的;在实践中,理论是误导性的废话,比没有更糟糕”阶段。没有人关心编译时发生的事情(对于非平凡的源代码编译时间是“O(我的上帝)”,因为会花费各种优化 - 例如,仅仅找出分配寄存器的理想方式是已知的“NP-hard ' 问题)和“在实践中运行时发生的事情”是唯一真正重要的事情。
    • 我不认为这是理论与实践。虽然您是正确的,优化器可以使用替代算法(改变程序的性能);声称这会以任何方式影响 原始函数 的复杂性是根本不正确的。也就是说,以下是错误的:“实际上,原始函数的时间复杂度取决于函数的使用方式以及编译器的好坏”(我猜这就是为什么被否决。)
    • 此外,我实际上认为,尽管对复杂性分析有更广泛的理解是有用的:OP 显然正在寻求对如何评估特定函数的理解。这不回答这个问题。相反,它似乎鼓励了懒惰,如:“它可以被优化,所以不要费心去想它。”恕我直言,理解理论和理解实践齐头并进。不能依靠编译器解决所有问题。而在实践中,您会想了解编译器未更改的代码如何影响性能。
    猜你喜欢
    • 2023-03-27
    • 2016-06-30
    • 1970-01-01
    • 2017-02-01
    • 2021-06-12
    • 2020-12-03
    • 2017-09-11
    • 2022-09-25
    • 2012-07-10
    相关资源
    最近更新 更多