为什么计算复杂度为O(n ^ 4)?


50
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次...我只是不知道它是什么。基本上,为什么j%我不能升至i * i而不是i?


5
您可以通过多个因素来降低此代码的复杂性。提示:1到n的总和是((n + 1)* n)/ 2 提示2for (j = i; j < i *i; j += i)则不需要进行模数测试(因为j保证可以被整除i)。
Elliott Frisch

1
O()函数是一个停球函数,因此此示例中的任何循环都会增加复杂性。第二个循环运行到n ^ 2。if语句被忽略。
Christoph Bauer

11
绝对不会忽略@ChristophBauer if语句。该语句意味着复杂度是O(n ^ 4)而不是O(n ^ 5),因为它使最内层的循环仅执行时间,而不是第二个循环的每次迭代的时间。ifii*i
kaya3

1
@ kaya3完全错过了该k < n^2部分,所以它是O(n ^ 5),但是知识(通过了解if)表明是O(n ^ 4)。
Christoph Bauer

1
如果这不只是课堂练习,请将第二个循环更改为for(int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

Answers:


49

让我们标记循环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 在A的每次迭代中迭代O(i 2)次。对于这些迭代中的每一个:
    • j % i == 0 被评估,这需要O(1)时间。
    • 在这些迭代的1 / i中,循环C迭代j次,每次迭代执行O(1)个工作。由于j平均为O(i 2),并且仅对循环B的1 / i次迭代完成,因此平均成本为O(i 2  /  i)= O(i)。

将所有这些相乘,我们得到O(n  ×  i 2  ×(1 +  i))= O(n  ×  i 3)。由于i平均为O(n),因此为O(n 4)。


棘手的部分是说if条件仅在1 / i时为真:

基本上,为什么j%我不能升至i * i而不是i?

实际上,j确实j < i * i不仅仅取决于j < i。但是,j % i == 0当且仅当j是的倍数时,条件才成立i

的倍数i范围内的i2*i3*i,..., (i-1) * i。其中有i - 1一些,因此i - 1尽管循环B迭代了时间,但到达循环C的i * i - 1次数。


2
在O(n×i ^ 2×(1 + i))中为什么是1 + i?
Soleil

3
因为该if条件在循环B的每次迭代上花费O(1)时间。在这里,该条件由循环C支配,但是我在上面进行了计数,因此它只是“显示了我的工作”。
kaya3

16
  • 第一个循环消耗n迭代。
  • 第二个循环消耗n*n迭代。当想象中的情况下i=n,然后j=n*n
  • 第三个循环消费n,因为它仅执行迭代i次,其中i为界,n在最坏的情况下。

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

希望这对您有所帮助。


6

所有其他答案都是正确的,我只想修改以下内容。我想看看,减少内部k循环的执行量是否足以降低下面的实际复杂度,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

  • 在Bachmann–Landau表示法家族表中,关于Big O表示法的Wikipedia页上~定义了两个操作数边的限制是相等的。要么:

    f渐近等于g

(我选择363作为排除的上限,因为这n = 362是我们得出明智结果的最后一个值。此后,我们超过了长空格,并且相对值变为负数。)

用户kaya3发现了以下内容:

顺便说一下,渐近常数正好是1/8 = 0.125;这是Wolfram Alpha提供的确切公式


5
当然,O(n⁴)* 0.125 = O(n⁴)。将运行时间乘以一个正常数将不会改变渐近复杂性。
Ilmari Karonen

这是真的。但是,我试图反映实际的复杂性,而不是上限估计。当我发现除了O表示法以外没有其他表达时间复杂性的语法时,我就回过头了。但是,这样写并不是100%明智的做法。
TreffnonX

您可以使用little-o表示时间复杂度为n⁴/8 + o(n⁴),但是n⁴/8 + O(n³)无论如何都可以使用big O 给出更严格的表达式。
kaya3

@TreffnonX大OH是一个数学上可靠的概念。因此,您所做的根本上是错误的/毫无意义的。当然,您可以自由地重新定义数学概念,但是那时候您将打开一大堆蠕虫。在更严格的上下文中定义它的方法就是kaya3所描述的,您下了一个“较低的”命令并以这种方式定义它。(尽管在数学中通常使用倒数)。
paul23

你是对的。我再次纠正了自己。这次,我使用渐近增长朝向相同的极限,这在en.wikipedia.org/wiki/Big_O_notation#Little-o_notation的巴赫曼-朗道符号系列中定义。我希望这在数学上已经足够正确,不会引起反叛了;)
TreffnonX

2

删除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直接从ito 跳转2*i3*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(请参阅三角数)。如果对每个循环都使用这种简化,则会得到:

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出现在“第一种斯特林数:s(n + 2,n)”中。0在开头添加两个。这意味着这sum第一种斯特林数 s(n, n-2)


0

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

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

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 loops1^2 + 2^2 + 3^2
前n个平方和有一个公式,n * (n+1) * (2n + 1) / 6大约为O(n^3)

您有一个倒数k loop从0循环到jif和only if j % i == 0。由于j从1到i^2,有时j % i == 0是正确的i。由于i loop迭代结束n,因此您又多了一个O(n)

所以,你必须O(n^3)i and j loops和另一个O(n)k loop一个总计O(n^4)


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

1
@ user11452926假设我是5。在第二个循环中,j从1变为25。但是,j % i == 0仅当j是i的5、10、15、20和25. 5倍时。如果您要在5 x 5平方中写下从1到25的数字,则只有第5列会包含被5整除的数字。这适用于任何数量的i。使用数字1到n ^ 2绘制n乘n的正方形。第n列将包含被n整除的数字。您有n行,因此从1到n ^ 2的n个数字可被n整除。
Silviu Burcea

谢谢!说得通!如果它是一个任意的数字,例如24而不是25,该平方数技巧仍然有效吗?
user11452926

i击中5 时25来了,所以j从1到25 的循环,您不能选择任意数字。如果您的第二个循环使用固定的数字(例如24),而不是i * i,则该数字将是一个恒定的数字,并且不会受到限制n,因此它将是O(1)。如果您正在考虑j < i * ivs. j <= i * i,那么就没有多大关系了,因为它会nn-1操作有关,但是在Big-oh表示法中,这两种方法都意味着O(n)
Silviu Burcea
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.