这两个嵌套循环真的拥有相同的二次时间复杂度吗?

3

这是我想出的一个算法片段:

for (int i = 0; i < n - 1; i++)
   for (int j = i; j < n; j++)
      (...)

我正在使用这个“双循环”来测试一个大小为n的数组中所有可能的2元素和。
显然(我必须同意),这个“双循环”是O(n²)的:
n + (n-1) + (n-2) + ... + 1 = sum from 1 to n = (n (n - 1))/2

以下是我感到困惑的地方:

for (int i = 0; i < n; i++)
   for (int j = 0; j < n; j++)
      (...)

这个第二个“双重循环”的复杂度也是O(n²),但它明显(最坏情况下)比第一个更好。我错过了什么?这个信息准确吗?有人能解释一下这个“现象”吗?

请参考相关问题http://programmers.stackexchange.com/questions/222895/complexity-in-nested-loops。 - Mihai8
5个回答

5

(n (n - 1))/2 简化为 n²/2 - n/2。如果你使用非常大的数来代替 n,那么 n/2 的增长速度将相对于 被忽略不计,在计算大O复杂度时你可以有效地忽略它。同样,常量值 1/2 不会随着 n 的增加而增长,所以也可以忽略它。这就只剩下

只要记住,复杂度计算与“速度”并不相同。一种算法可能比另一种慢五千倍,但仍然具有更小的大O复杂度。但是,随着 n 增大到非常大的数字,通常会出现一般模式,可以使用简单的公式进行分类: 1log nnn log n等。

有时候创建一个图表并观察出现的线条类型有帮助:

y=x² y=(x² - x)/2

即使这两张图的缩放因子非常不同,你也可以看到它产生的曲线类型几乎完全相同。


那么基本上,由于它们在渐近意义下是相同的,我使用第一个 n^2 - n/2 算法没有任何理由优于第二个 n^2 算法,对吧? - Werner
@Werner:我在我的答案中添加了更多信息。虽然这两个算法在渐近意义下是相同的,但这并不意味着它们的速度没有差异:这只是意味着随着数字变得越来越大,速度差异就不太可能有影响:比如第一个算法需要运行1,000,000秒,而第二个算法只需要运行499,500秒。第二个算法将快两倍,但有多少人愿意等待5天让他们的程序运行,而不愿意等待10天?要知道在实践中使用哪个算法,您需要比Big-O数字更多的信息。 - StriplingWarrior
如果有选择的话,我会争辩说没有人会愿意等待x时间,当他们可以选择只等待~x/2的时间时...因此,大O符号在帮助我决定使用哪种算法方面并不是一个有用的指标?它只对"极其"不同的O(例如O(n) vs O(n^2))的算法有用...? - Werner
@Werner:就我而言,如果我需要等待10微秒而不是5微秒来获取所需的信息,我并不在意。这两个选项都足够快。同样,即使替代方案是等待10天,等待5天也太久了。(对我来说10秒通常也太久了!)大O表示法非常有用,因为它有助于确定算法在更可能影响结果的大数据集上渐进地更快。如果没有大O的区别,一种算法仍然可能比另一种算法快得多,但这可能无关紧要:要么找到能在<100毫秒内运行的程序,否则就别费力了。 - StriplingWarrior
@Werner:所以在这种情况下,大O符号只有在告诉你如果N变得非常大,这两个算法都会变得极其缓慢的程度上是有用的。如果你想知道哪一个真正更快,你很可能需要在实际数据集上测试它们。但无论如何,这通常是一个好的经验法则:“过早优化是万恶之源”。 - StriplingWarrior

2

常数因子。

大O符号忽略常数因子,所以即使第二个循环比第一个慢一个常数因子,它们最终的时间复杂度相同。

定义中,它告诉你可以选择任何旧的常数因子:

……当且仅当存在正常数M……

这是因为我们想要分析算法的增长率-常数因子只会使事情变得复杂,并且通常是系统相关的(在不同的机器上操作可能持续不同的时间)。

您可以只计算某些类型的操作,但问题在于选择哪种操作,如果该操作在某些算法中不占主导地位怎么办。然后,您将需要以一种相互关联的方式来比较操作(以系统无关的方式,这可能是不可能的),或者您可以只为每个操作分配相同的权重,但这将是相当不准确的,因为有些操作会比其他操作花费更长的时间。

说出 O(15n² + 568n + 8 log n + 23 sqrt(n) + 17)(例如)有多有用?与仅使用 O(n²) 相比。


(为了下面的目的,假设 n >= 2

请注意,这里实际上有渐近更小的项(即当我们接近无穷大时更小),但我们始终可以将其简化为常数因子的问题。(它是n(n+1)/2,而不是n(n-1)/2

n(n+1)/2 = n²/2 + n/2
       and
n²/2 <= n²/2 + n/2 <= n²

鉴于我们刚刚证明了对于两个常数C和D,n(n + 1) / 2位于C.n²和D.n²之间,我们也证明了它是O(n²)。 注意 - 大O符号实际上严格意义上只是一个上界(因此我们只关心它比一个函数小,而不是在两个函数之间),但它通常用来表示Θ(大Theta),它关心两个边界。

@Paul,可以将其简化为常数因子的差异。渐近地,n² - n < 2n² - Bernhard Barker

0

来自维基百科上的大O页面

在典型的用法中,不直接使用O符号的正式定义;相反,通过以下简化规则导出函数f的O符号:
如果f(x)是几个项的总和,则保留增长率最大的一项,并省略所有其他项

大O符号仅用于给出渐近行为 - 其中一个比另一个快一点并不重要 - 它们都是O(N^2)


0

你也可以说第一个循环是 O(n(n-1)/2)。大O的花式数学定义是:

如果存在常量c和n,使得对于某些c和所有x > n,f(x) < c*g(x),则函数"f"是函数"g"的大O。

这是一种说法,即在某一点之后,g是一个上界,加上一些常数。因此,O(n(n-1)/2) = O((n^2-n)/2)是O(n^2)的大O,这更方便快速分析。


0

据我所知,你的第二段代码片段

for(int i = 0; i < n; i++) <-- this loop goes for n times
     for(int j = 0; j < n; j++) <-- loop also goes for n times
          (...)

因此,它的时间复杂度是O(n*n) = O(n^2)

根据BIG-O理论,会忽略常数因子,只考虑更高阶的影响。也就是说,如果复杂度为O(n^2+k),那么实际复杂度将是O(n^2),常数k将被忽略。

或者,如果复杂度是O(n^2+n),则实际复杂度将是O(n^2),低阶的n将被忽略。

因此,在你的第一个情况下,复杂度为O(n(n - 1)/2)可以简化为

O(n^2/2 - n/2) = O(n^2/2) (Ignoring the lower order n/2)
= O(1/2 * n^2)
= O(n^2) (Ignoring the constant factor 1/2)

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