时间复杂度
定义
衡量一个算法的快慢,一定要考虑数据规模的大小。所谓数据规模,一般指输入的数字个数、输入中给出的图的点数与边数等等。一般来说,数据规模越大,算法的用时就越长。而在算法竞赛中,我们衡量一个算法的效率时,最重要的不是看它在某个数据规模下的用时,而是看它的用时随数据规模而增长的趋势,即 时间复杂度。
——OI Wiki
在算法中,使用时间复杂度来衡量程序运行时间的多少。每条语句执行的次数称为该语句的频度,整段代码的总执行次数则称为整段代码的频度。在算法竞赛中,由于输入可以在给定的数据范围内任意给定,我们为保证算法能够通过某个数据范围内的任何数据,一般考虑最坏时间复杂度。
下面,我将用更通俗易懂的语言,解释时间复杂度。
分析
在推导的时候,我们应该采用无限大的思想来简化大 O O O 表达式,具体如下:
1.用常数 1 1 1 代替运行时间中的所有加法的常数。无论常数是多少都使用 1 1 1 替换。因为执行函数和问题规模 n n n 的大小无关,它是执行时间恒定的,像时间复杂度为 O ( 1 ) O(1) O(1) 的又被称作常数阶。如:执行 100 100 100 次 1 → n 1 \to n 1→n 的循环,这份代码的时间复杂度为 O ( n ) O(n) O(n) 而并非 O ( 100 n ) O(100n) O(100n)。
2.如果最高阶项存在且系数不为 1 1 1,则去除掉与这个项相乘的系数,例如 O ( 2 n 2 ) O(2n^2) O(2n2) 系数为 2 2 2,直接简化为 O ( n 2 ) O(n^2) O(n2)。
3.如果表达式有多项含有无限大变量的式子,只保留一个拥有指数最高的变量的式子。如 O ( 2 n 2 + 2 n ) O(2n^2+2n) O(2n2+2n) 应简化为 O ( n 2 ) O(n^2) O(n2)。
经过上面三个步骤推到出来的结果就是算法对应的大 O O O 阶。
注意,在遇到类似于 O ( 2 n + m ) O(2n+m) O(2n+m) 的时间复杂度时,由于 n n n 和 m m m 之间没有联系,所以这份代码的时间复杂度应是 O ( n + m ) O(n+m) O(n+m)。在遇到类似于 O ( n 2 + n q ) O(n^2+nq) O(n2+nq) 的时间复杂度时,由于我们无法确定 n 2 n^2 n2 和 n q nq nq 的大小( n n n 和 q q q 的大小不确定),因此这份代码的时间复杂度依然是 O ( n 2 + n q ) O(n^2+nq) O(n2+nq)。
举例:
-
O ( 12 ) → O ( 1 ) O(12)\to O(1) O(12)→O(1)
-
O ( 2 n + 3 ) → O ( n ) O(2n+3)\to O(n) O(2n+3)→O(n)
-
O ( 3 n 2 + 2 n + 1 ) → O ( n 2 ) O(3n^2+2n+1)\to O(n^2) O(3n2+2n+1)→O(n2)
-
O ( 5 log 2 n + 20 ) → O ( log n ) O(5\log_2 n+20)\to O(\log n) O(5log2n+20)→O(logn)
-
O ( 2 n + 3 n log 2 n + 19 ) → O ( n log n ) O(2n+3n\log_2 n+19)\to O(n\log n) O(2n+3nlog2n+19)→O(nlogn)
-
O ( 6 n 3 + 2 n 2 + 3 n + 4 ) → O ( n 3 ) O(6n^3+2n^2+3n+4)\to O(n^3) O(6n3+2n2+3n+4)→O(n3)
-
O ( 2 n + n 4 ) → O ( 2 n ) O(2^n+n^4)\to O(2^n) O(2n+n4)→O(2n)
Another
哪些数是常数?
我们曾提到,“用常数 1 1 1 代替运行时间中的所有加法的常数”,那么,什么是常数呢?
我们一个例子看起。
const int n=114514;
for (int i=1;i<=n;i++){
cout<<"hello world\n";
}
代码中, M A X N MAXN MAXN 虽然是一个变量,但是它的值是固定的,所以它不被看做输入数据规模,所以代码的时间复杂度是 O ( 1 ) O(1) O(1) 而不是 O ( n ) O(n) O(n)。
进行时间复杂度计算时,哪些变量被视作输入规模是很重要的,而所有和输入规模无关的量都被视作常量,计算复杂度时可当作 1 1 1 来处理。