时间复杂度大 O - 当我们有 2 个嵌套的 FOR 循环时,N 的值能否决定时间复杂度是 O(1) 还是 O(N)?

问题描述

假设我有 2 个嵌套的 for 循环和 1 个大小为 N 的数组,如下面的代码所示:

int result = 0;

for( int i = 0; i < N ; i++)
{
    for( int j = i; j < N ; j++)
    {
        result = array[i] + array[j]; // just some funny operation
    }
}

这里有两种情况:

(1) 如果严格约束是 N >= 1,000,000,那么我们可以肯定地说时间复杂度是 O(N^2)。众所周知,这是肯定的。

(2) 现在,如果严格约束是 N 使用现代计算机?听起来对吗?

请告诉我 N 的值是否在决定时间复杂度 O(N) 的结果中起作用?如果是,那么 N 需要多大才能扮演这个角色(1,000 ? 5,000 ? 20,000 ? 500,000 ?)换句话说,这里的一般经验法则是什么?


有趣的理论问题:如果 15 年后,计算机速度如此之快,即使 N = 25,000,这 2 个 for 循环也可以在 1 秒内完成。那时,我们可以说即使 N = 25,000 的时间复杂度也是 O(1) 吗?我想当时的答案是。你同意吗?

解决方法

tl:dr 否。N 的值对时间复杂度没有影响。 O(1) vs O(N) 是关于“所有 N”或当 N 增加时计算量如何增加的陈述。

好问题!这让我想起了我第一次尝试理解时间复杂度的时候。我想很多人都必须经历类似的过程才能开始有意义,所以我希望这个讨论可以帮助其他人。

首先,您的“有趣操作”实际上比您想象的更有趣,因为您可以将整个嵌套的 for 循环替换为:

result = array[N - 1] + array[N - 1]; // just some hilarious operation hahaha ha ha

由于每次都覆盖 result,因此只有最后一次迭代才会影响结果。我们会回到这个话题。

至于您在这里真正要问的是什么,Big-O 的目的是提供一种有意义的方式来比较算法,独立于输入大小且独立于计算机的处理速度强>。换句话说,O(1) 与 O(N) 与 N 的大小无关,也与您的计算机的“现代”程度无关。这一切都会影响算法在具有特定输入的特定机器上的执行时间,但不影响时间复杂度,即 O(1) 与 O(N)。

这实际上是关于算法本身的声明,因此数学讨论是不可避免的,正如 dxiv 在他的评论中如此亲切地提到的那样。免责声明:我将省略数学中的某些细微差别,因为关键的东西已经有很多要解释的了,我将遵循网络和教科书上其他地方的大量完整解释。

您的代码是了解 Big-O 做什么告诉我们什么的一个很好的例子。你写它的方式,它的复杂度是 O(N^2)。这意味着无论你在什么机器或什么时代运行你的代码,如果你要计算计算机必须执行的操作数量,对于每个 N,并将其绘制为一个函数,比如 f(N),存在一些二次函数,比如 g(N)=9999N^2+99999N+999,对于所有 N 都大于 f(N)。

但是等等,如果我们只需要找到足够大的系数以使 g(N) 成为上限,我们就不能声称算法是 O(N) 并找到一些 g(N)= aN+b 具有足够大的系数,它是 f(N) 的上限??? 这个问题的答案是您需要了解的最重要的数学观察,才能真正理解 BIG-O 符号。剧透警报。答案是否定的。

对于视觉效果,请在 Desmos 上尝试此图表,您可以在其中调整系数:[https://www.desmos.com/calculator/3ppk6shwem][1]

无论您选择什么系数,aN^2+bN+c 形式的函数最终都会超过 aN+b 形式的函数(两者都具有正 a)。您可以像 g(N)=99999N+99999 一样将一条线推高,但即使函数 f(N)=0.01N^2+0.01N+0.01 也越过该线并在 N=9999900 之后越过它。没有线性函数是二次方程的上界。类似地,不存在作为线性函数或二次函数上限的常数函数。然而,我们可以找到这个 f(N) 的二次上限,例如 h(N)=0.01N^2+0.01N+0.02,所以 f(N) 在 O(N^2) 中。这个观察使我们可以只说 O(1) 和 O(N^2) 而不必区分 O(1)、O(3)、O(999)、O(4N+3)、O(23N) +2)、O(34N^2+4+e^N) 等。通过使用“存在一个函数使得”这样的短语,我们可以把所有的常数系数都刷掉。

所以有一个二次上界,也就是在 O(N^2) 中,意味着函数 f(N) 不大于二次,在这种情况下恰好是二次。听起来这只是比较多项式的次数,为什么不直接说该算法是2次算法呢?为什么我们需要这个超级抽象“存在一个上限函数使得bla bla bla...”?这是 Big-O 解释非多项式函数所必需的概括,一些常见的函数是 logN、NlogN 和 e^N。

例如,如果您的算法所需的运算次数由 f(N)=floor(50+50*sin(N)) 给出,我们会说它是 O(1),因为有一个常数函数,例如g(N)=101 这是 f(N) 的上限。在这个例子中,你有一些奇怪的算法,执行时间不稳定,但是你可以通过简单地说它是 O(1) 来向其他人传达它对于大输入不会减慢多少。整洁的。另外,我们有一种方法可以有意义地说,这种具有三角执行时间的算法比具有线性复杂度 O(N) 的算法更有效。整洁的。请注意计算机有多快并不重要,因为我们不是在以秒为单位进行测量,而是在操作中进行测量。所以你可以在纸上手工评估算法,即使它需要你一整天,它仍然是 O(1)。

至于您问题中的示例,我们知道它是 O(N^2),因为某些 a、b、c 涉及 aN^2+bN+c 操作。它不可能是 O(1),因为无论你选择什么 aN+b,我都能找到足够大的输入大小 N,这样你的算法需要的操作不止 aN+b。在任何计算机上,在任何时区,外面可能下雨。没有物理影响 O(1) 与 O(N) 与 (N^2)。 将其更改为 O(1) 是将算法本身更改为 到我在上面提供的单行代码,您只需将两个数字相加并吐出结果,无论 N 是多少。假设 N=10 需要 4 次操作来执行数组查找、加法和变量赋值。如果在 N=10000000 的同一台机器上再次运行它,它仍然会执行相同的 4 次操作。算法所需的运算量不随 N 增长。这就是算法是 O(1) 的原因。

这就是为什么像寻找 O(NlogN) 算法对数组进行排序这样的问题是数学问题而不是纳米技术问题的原因。 Big-O 甚至不认为你有一台带电子设备的电脑。

希望这个咆哮能提示您理解的内容,以便您可以进行更有效的学习以获得完整的理解。没有办法在这里的一篇文章中涵盖所有需要的内容。这对我来说是一次很好的自我反省,所以谢谢。