复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
1、大 O 复杂度表示法
1 int cal(int n) { 2 int sum = 0; 3 int i = 1; 4 int j = 1; 5 for (; i <= n; ++i) { 6 j = 1; 7 for (; j <= n; ++j) { 8 sum = sum + i * j; 9 } 10 } 11 }
第 2、3、4 行代码,每行都需要 1 个 unit_time 的执行时间,第 5、6 行代码循环执行了 n 遍,需要 2n * unit_time 的执行时间,第 7、8 行代码循环执行了 n2遍,所以需要 2n2* unit_time 的执行时间。所以,整段代码总的执行时间 T(n) = (2n2+2n+3)*unit_time。用大 O 表示时间复杂度T(n) = O(n2)。
规律:所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。
公式:
含义:T(n) 表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
定义:大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示这段代码的时间复杂度,就可以记为: T(n) = O(n2)。
2、复杂度分析法则
1)单段代码看高频:比如循环。
2)多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。
3)嵌套代码求乘积:比如递归、多重循环等
4)多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加。
3、几种常见时间复杂度实例分析
多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。包括,O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n2)(平方阶)、O(n3)(立方阶)
非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括,O(2n)(指数阶)、O(n!)(阶乘阶)
案例:
1、O(1) 常数阶:一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。
1 int i = 8; 2 int j = 6; 3 int sum = i + j;
2、O(logn) 对数阶:
1 i=1; 2 while (i <= n) { 3 i = i * 2; 4 }
第三行代码是循环执行次数最多的,只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:
所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2x=n 求出x=log2n,所以,这段代码的时间复杂度就是 O(log2n)。但是当n无穷大的时候,忽略对数的“底”,统一表示为 O(logn)。
3、O(nlogn) 线性对数阶: 如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。
4、O(m+n)、O(m*n) :代码的复杂度由两个数据的规模来决定。如下面的代码时间复杂度就是 O(m+n)。
1 int cal(int m, int n) { 2 int sum_1 = 0; 3 int i = 1; 4 for (; i < m; ++i) { 5 sum_1 = sum_1 + i; 6 } 7 8 int sum_2 = 0; 9 int j = 1; 10 for (; j < n; ++j) { 11 sum_2 = sum_2 + j; 12 } 13 14 return sum_1 + sum_2; 15 }