在计算复杂性这个话题上,我有点困惑。
我知道大O,也知道如何计算循环的复杂度(嵌套的也是)。
假设我有一个从1到n运行3个循环的程序
for (int i=0;i<n;i++)
{
cout << i ;
}
现在如果我运行我的CPP代码有3个for循环,它会花费3*N的时间吗?
CPP编译器是同时运行所有3个循环还是一个接一个地运行?
我对这个话题感到很困惑。 请救命!
现在如果我运行我的CPP代码有3个for循环,它会花费3*N的时间吗?
是的,假设每次循环迭代的时间是相同的,但是在大O表示法O(3*n)==O(n)
中,所以复杂度仍然是线性的。
CPP编译器会同时运行所有的3个循环还是会一个接一个地运行?
隐式并发要求编译器100%确信并行化代码不会改变结果。 对于简单的操作可以(而且确实是这样,请参见注释),但是cout<<<; i
不太可能被并行化。 但是,它可以以不同的方式进行优化,例如,如果在编译时已知n
,编译器可以一次性生成整个字符串,并将循环改为cout<<<; “123456.。。”;
。
此外,时间复杂性和并发性是相当不相关的主题。 在20个线程上执行的代码将具有与在一个线程上执行的代码相同的复杂性,它只是更快(或者不是)。
现在如果我运行我的CPP代码有3个for循环,它会花费3*N的时间吗?
运行一千个循环,仍然是O(n)
,因为在计算函数的时间复杂度上限时,忽略了任何常数。 因此,如果m不依赖于输入大小,O(n*m)
将始终为O(n)。
此外,编译器不会同时运行它们,而是依次运行(除非是多线程,ofc)。 但即使这样,根据定义,只要循环的次数不依赖于输入大小,3个,10个或1000个循环可能会被认为是o(n)
。
如果一个代码包含多个n个复杂度循环,如何计算复杂度?
为了理解大O表示法和渐近复杂性,至少求助于半形式表示法是有用的。
考虑了基于n
增长的函数f(n)
的渐近时间复杂度的求取及上界问题。
为了帮助我们,让我们在O(g(n))
中松散地定义一个函数或算法f
(为了挑剔,O(g(n))
是一组函数,因此f∈O(...)
,而不是通常被误用的f(n)∈O(...)
),如下所示:
如果函数f
在O(g(n))
中,则对于某些非负常数c
,对于足够大的n
(即对于某些常数n0
n≥n0
),c·g(n)
,c·g(n)
是f(n)
的上界。
因此,为了证明f∈O(g(n))
,我们需要找到一组满足以下条件的(非负)常数(c,n0)
f(n) ≤ c · g(n), for all n ≥ n0, (+)
让我们考虑一下你的实际问题
void foo(int n) {
for (int i = 0; i < n; ++i) { std::cout << i << "\n"; }
for (int i = 0; i < n; ++i) { std::cout << i << "\n"; }
for (int i = 0; i < n; ++i) { std::cout << i << "\n"; }
}
为了分析foo
基于n
增长的渐近行为,请考虑std::cout<<; i<<<; “\n”;
作为我们的基本操作。 因此,基于该定义,foo
包含3*n
基本操作,我们可以在数学上将foo
考虑为
f(n) = 3 * n.
现在,我们需要找到一个G(n)
和一组常量C
和N0
,以便(+)
能够成立。 对于这个特定的分析,这几乎是微不足道的; 在(+)
中插入如上的f(n)
,并使g(n)=n
:
3 * n ≤ c · g(n), for all n ≥ n0, [let g(n) = n]
3 * n ≤ c · n, for all n ≥ n0, [choose c = 3]
3 * n ≤ 3 · n, for all n ≥ n0.
后者适用于任何有效的n
,我们可以任意选择n0=0
。 因此,根据我们上面对函数f
在O(g(n))
中的定义,我们已经证明了f
在O(n)
中。
很明显,即使我们将foo
中的循环乘以倍数,只要这个倍数是常数(并且不依赖于n
本身),我们总是可以找到将满足g(n)=n
的(+)
的退化数目的常数c
和n
,从而表明描述foo
中基于n
的基本操作数目的函数f
是线性增长的上限。
现在如果我运行我的CPP代码有3个for循环,它会花费3*N的时间吗?
然而,理解Big-O表示法描述数学描述的算法的渐近行为的上界是至关重要的,或者例如,基于基本操作的定义可以描述为前者的编程实现的函数的渐近行为的上界。 然而,它并没有提供一个精确的描述,您可能期望的运行时如何实现一个函数的不同变体。 高速缓存局部性,并行/向量化,编译器优化和硬件本质,描述基本操作的不准确性,这些都是导致渐近复杂度与实际运行时间脱节的众多因素中的几个。 链表数据结构是一个很好的例子,其中渐近分析不太可能给出运行时性能的良好视图(因为缓存位置的丢失,列表的实际大小等可能会产生更大的影响)。
对于算法的实际运行时间,如果遇到瓶颈,那么在目标硬件上使用产品代表编译器和优化标志进行实际测量是关键。