【问题标题】:Why is the computational complexity O(n^4)?为什么计算复杂度为 O(n^4)?
【发布时间】:2020-03-11 00:20:34
【问题描述】:
int sum = 0;
for(int i = 1; i < n; i++) {
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}

我不明白什么时候 j = i, 2i, 3i... 最后一个 for 循环运行 n 次。我想我只是不明白我们是如何根据if 声明得出这个结论的。

编辑:我知道如何计算所有循环的复杂性,除了为什么最后一个循环基于 mod 运算符执行 i 次......我只是不明白它是如何 i 的。基本上,为什么 j % i 不能达到 i * i 而不是 i?

【问题讨论】:

  • 您可以通过多次降低此代码的复杂性大的因素。暗示: 数字 1 到 n 的和是 ((n+1)*n)/2提示 2:for (j = i; j &lt; i *i; j += i) 那么你不需要模数测试(因为j 保证可以被i 整除)。
  • O() 函数是一个球场函数,因此此示例中的任何循环都会增加复杂性。第二个循环运行到 n^2。 if 语句被忽略。
  • @ChristophBauer if 声明是绝对不忽略。这个if 语句意味着复杂度是O(n^4) 而不是O(n^5),因为它导致最内层循环只执行i 次而不是i*i 次对于第二个循环的每次迭代.
  • @kaya3 完全错过了 k &lt; n^2 部分。所以它是 O(n^5),但知识(通过理解 if)建议 O(n^4)。
  • 如果这不仅仅是课堂练习,请将第二个循环更改为 for(int j = i; j < i * i; j+=i)

标签: java big-o


【解决方案1】:

让我们标记循环 A、B 和 C:

int sum = 0;
// loop A
for(int i = 1; i < n; i++) {
    // loop B
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            // loop C
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}
  • 循环 A 迭代 O(n) 次。
  • 循环 B 迭代 O(一世2) 次A 的每次迭代.对于这些迭代中的每一个:
    • j % i == 0 被评估,这需要 O(1) 时间。
    • 在 1/一世在这些迭代中,循环 C 迭代j次,每次迭代做 O(1) 工作。自从j是 O(一世2) 平均而言,这仅适用于 1/一世循环B的迭代,平均成本为O(一世2 / 一世) = O(一世)。

将所有这些相乘,我们得到 O(n×一世2× (1 +一世)) = O(n×一世3)。自从一世平均为 O(n), 这是 O(n4)。


棘手的部分是说if 条件仅为真 1/一世的时间:

基本上,为什么 j % i 不能达到 i * i 而不是 i?

事实上,j 确实上升到 j &lt; i * i,而不仅仅是上升到 j &lt; i。但条件j % i == 0 为真当且仅当j 是i 的倍数。

范围内i的倍数为i、2*i、3*i、...、(i-1) * i。其中有i - 1,因此尽管循环B 迭代i * i - 1 次,但循环C 仍到达i - 1 次。

【讨论】:

  • 在 O(n × i^2 × (1 + i)) 为什么 1+i ?
  • 因为if 条件在循环 B 的每次迭代中都需要 O(1) 时间。它在这里由循环 C 主导,但我在上面计算了它,所以它只是“展示我的工作”。
【解决方案2】:
  • 第一个循环消耗n 迭代。
  • 第二个循环消耗n*n 迭代。想象一下i=n,然后是j=n*n 的情况。
  • 第三个循环消耗n 迭代,因为它只执行i 次,其中i 在最坏的情况下绑定到n。

因此,代码复杂度为 O(n×n×n×n)。

我希望这能帮助你理解。

【讨论】:

    【解决方案3】:

    所有其他答案都是正确的,我只想修改以下内容。 我想看看,如果减少内部 k-loop 的执行是否足以将实际复杂度降低到O(n⁴). 以下,所以我写了以下内容:

    for (int n = 1; n < 363; ++n) {
        int sum = 0;
        for(int i = 1; i < n; ++i) {
            for(int j = 1; j < i * i; ++j) {
                if(j % i == 0) {
                    for(int k = 0; k < j; ++k) {
                        sum++;
                    }
                }
            }
        }
    
        long cubic = (long) Math.pow(n, 3);
        long hypCubic = (long) Math.pow(n, 4);
        double relative = (double) (sum / (double) hypCubic);
        System.out.println("n = " + n + ": iterations = " + sum +
                ", n³ = " + cubic + ", n⁴ = " + hypCubic + ", rel = " + relative);
    }
    

    执行此操作后,很明显,复杂度实际上是n⁴。输出的最后几行如下所示:

    n = 356: iterations = 1989000035, n³ = 45118016, n⁴ = 16062013696, rel = 0.12383254507467704
    n = 357: iterations = 2011495675, n³ = 45499293, n⁴ = 16243247601, rel = 0.12383580700180696
    n = 358: iterations = 2034181597, n³ = 45882712, n⁴ = 16426010896, rel = 0.12383905075183874
    n = 359: iterations = 2057058871, n³ = 46268279, n⁴ = 16610312161, rel = 0.12384227647628734
    n = 360: iterations = 2080128570, n³ = 46656000, n⁴ = 16796160000, rel = 0.12384548432498857
    n = 361: iterations = 2103391770, n³ = 47045881, n⁴ = 16983563041, rel = 0.12384867444612208
    n = 362: iterations = 2126849550, n³ = 47437928, n⁴ = 17172529936, rel = 0.1238518469862343
    

    这表明,实际 n⁴ 与此代码段的复杂性之间的实际相对差异是一个渐近于 0.124... 附近的值(实际上是 0.125)的因素。虽然它没有给我们确切的值,但我们可以推断如下:

    时间复杂度为n⁴/8 ~ f(n),其中f 是您的函数/方法。

    • Wikipedia-page on Big O notation 在“Bachmann–Landau 符号家族”的表格中指出,~ 定义了两个操作数边的限制相等。或者:

      f 渐近地等于 g

    (我选择 363 作为排除上限,因为n = 362 是我们得到合理结果的最后一个值。之后,我们超过了长空间,相对值变为负数。)

    用户 kaya3 发现以下内容:

    顺便说一句,渐近常数正好是 1/8 = 0.125; here's the exact formula via Wolfram Alpha。

    【讨论】:

    • 当然,O(n⁴) * 0.125 = O(n⁴)。将运行时间乘以一个正常数因子不会改变渐近复杂度。
    • 这是真实的。但是我试图反映实际的复杂性,而不是上限估计。由于我发现除了 O 表示法之外没有其他表达时间复杂度的语法,所以我依靠它。然而,这样写并不是 100% 明智的。
    • 您可以使用little-o notation 表示时间复杂度为n⁴/8 + o(n⁴),但无论如何也可以使用大O 给出更严格的表达式n⁴/8 + O(n³)。
    • @TreffnonX big OH 是一个数学坚实的概念。所以你在做什么从根本上是错误的/毫无意义的。当然你可以自由地重新定义数学概念,但那是你打开的一大堆蠕虫。在更严格的上下文中定义它的方法是 kaya3 所描述的,你去一个“更低”的顺序并以这种方式定义它。 (尽管在数学中您通常使用互惠)。
    • 你是对的。我又纠正了自己。这一次,我将渐近增长用于相同的极限,正如en.wikipedia.org/wiki/Big_O_notation#Little-o_notation 上的巴赫曼-朗道家族符号中所定义的那样。我希望现在这在数学上足够正确,不会煽动叛乱;)
    【解决方案4】:

    删除if 和模而不改变复杂度

    这是原始方法:

    public static long f(int n) {
        int sum = 0;
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < i * i; j++) {
                if (j % i == 0) {
                    for (int k = 0; k < j; k++) {
                        sum++;
                    }
                }
            }
        }
        return sum;
    }
    

    如果你对 if 和模数感到困惑,你可以重构它们,j 直接从 i 跳转到 2*i 到 3*i ... :

    public static long f2(int n) {
        int sum = 0;
        for (int i = 1; i < n; i++) {
            for (int j = i; j < i * i; j = j + i) {
                for (int k = 0; k < j; k++) {
                    sum++;
                }
            }
        }
        return sum;
    }
    

    为了更容易计算复杂度,您可以引入一个中介 j2 变量,以便每个循环变量在每次迭代时递增 1:

    public static long f3(int n) {
        int sum = 0;
        for (int i = 1; i < n; i++) {
            for (int j2 = 1; j2 < i; j2++) {
                int j = j2 * i;
                for (int k = 0; k < j; k++) {
                    sum++;
                }
            }
        }
        return sum;
    }
    

    您可以使用调试或老式System.out.println 来检查i, j, k 三元组在每种方法中是否始终相同。

    封闭式表达式

    正如其他人所提到的,您可以使用第一个n 整数之和等于n * (n+1) / 2 的事实(请参阅triangular numbers)。如果对每个循环都使用这种简化,你会得到:

    public static long f4(int n) {
        return (n - 1) * n * (n - 2) * (3 * n - 1) / 24;
    }
    

    很明显不是与原始代码相同的复杂性,但它确实返回相同的值。

    如果你用谷歌搜索第一个术语,你会注意到0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731 出现在"Stirling numbers of the first kind: s(n+2, n)." 中,并且在开头添加了两个0s。这意味着sum 是Stirling number of the first kind s(n, n-2)。

    【讨论】:

      【解决方案5】:

      让我们看一下前两个循环。

      第一个很简单,它从 1 循环到 n。第二个更有趣。它从 1 到 i 平方。让我们看一些例子:

      e.g. n = 4    
      i = 1  
      j loops from 1 to 1^2  
      i = 2  
      j loops from 1 to 2^2  
      i = 3  
      j loops from 1 to 3^2  
      

      总共,i and j loops 组合有1^2 + 2^2 + 3^2。
      前n个平方和有一个公式,n * (n+1) * (2n + 1) / 6,大致是O(n^3)。

      当且仅当j % i == 0 时,您有最后一个k loop 从0 循环到j。由于 j 从 1 变为 i^2,因此 j % i == 0 在 i 次中为真。由于i loop 迭代了n,因此您多了一个O(n)。

      所以你有来自i and j loops的O(n^3)和来自k loop的另一个O(n),总共有O(n^4)

      【讨论】:

      • 我知道如何计算所有循环的复杂性,除了为什么最后一个循环基于 mod 运算符执行 i 次......我只是不明白它是如何 i 的。基本上,为什么 j % i 不能达到 i * i 而不是 i?
      • @user11452926 假设 i 是 5。 j 在第二个循环中会从 1 变为 25。但是j % i == 0只在j为5、10、15、20、25的时候。5倍,和i的值一样。如果您在 5 x 5 的正方形中写下从 1 到 25 的数字,则只有第 5 列包含可被 5 整除的数字。这适用于任何数量的 i。使用数字 1 到 n^2 绘制 n×n 的正方形。第 n 列将包含可被 n 整除的数字。你有 n 行,所以从 1 到 n^2 的 n 个数字可以被 n 整除。
      • 谢谢!说得通!如果它是一个像 24 而不是 25 这样的任意数字,平方的把戏还能用吗?
      • 25 在i 命中 5 时出现,因此j 从 1 循环到 25,您不能选择任意数字。如果您的第二个循环将转到一个固定数字,例如24,而不是i * i,这将是一个常数并且不会绑定到n,所以它将是O(1)。如果您正在考虑 j &lt; i * i 与 j &lt;= i * i,这并不重要,因为会有 n 和 n-1 操作,但在 Big-oh 符号中,两者都表示 O(n)
      猜你喜欢
      • 2018-11-26
      • 1970-01-01
      • 2021-11-11
      • 2011-09-03
      • 1970-01-01
      • 2016-06-07
      • 2013-01-28
      • 2021-06-22
      • 2022-10-24
      相关资源
      最近更新 更多