文章目录
数据结构与算法|第一章:复杂度分析
1.项目环境
- jdk 1.8
- github 地址:https://github.com/huajiexiewenfeng/data-structure-algorithm
- 本章模块:chapter01
2.大 O 复杂度表示法
如何计算一段代码的时间复杂度?
2.1 示例1
- 假设每行代码执行的时间一样,为 unit_time
- 本方法执行总时间为多少
public static int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; i++) {
sum = sum + i;
}
return sum;
}
第 2 行和第 3 行执行时间为 2*unit_time
第4 行和第 5 行是一个 for 循环,执行时间为 2n*unit_time
所以方法执行的总时间 T(n) = (2+2n)*unit_time
2.2 示例2
public static int cal2(int n) {
int sum = 0;
int i = 1;
for (; i <= n; i++) {
int j = 1;
for (; j < n; j++) {
sum = sum + i;
}
}
return sum;
}
第一段(2行,3行):2*unit_time
第二段(4行,5行):2n*unit_time
第三段(6行,7行): 2 n 2 2n^2 2n2*unit_time
方法执行的总时间 T(n) = ( 2 + 2 n + 2 n 2 (2+2n+2n^2 (2+2n+2n2)*unit_time
2.3 复杂度公式
将上面的例子总结成一个公式:
T
(
n
)
=
O
(
f
(
n
)
)
T(n) = O(f(n))
T(n)=O(f(n))
-
T(n) 它表示代码执行的时间;n 表示数据规模的大小
-
f(n) 表示每行代码执行的次数总和
因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
第一个例子复杂度公式:
T ( n ) = O ( 2 n + 2 ) T(n) =O(2n+2) T(n)=O(2n+2)
第二个例子复杂度公式:
T ( n ) = O ( 2 + 2 n + 2 n 2 ) T(n) = O (2+2n+2n^2) T(n)=O(2+2n+2n2)
当 n 很大时,你可以把它想象成10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为: T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n), T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)。
3.时间复杂度分析
3.1 只关注循环执行次数最多的一段代码
public static int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; i++) {
sum = sum + i;
}
return sum;
}
这个例子中循环次数最多的是第 4 行和第 5 行代码,所以时间复杂度 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)
3.2 加法法则:总复杂度等于量级最大的那段代码的复杂度
/**
* 加法法则:T(n) = O(max(f(n), g(n)))
* @param n
*/
public static void cal3(int n){
cal(n);//T1(n)=O(f(n))
cal2(n);//T2(n)=O(g(n))
}
方法1 cal(n) 的时间复杂度: T 1 ( n ) = O ( f ( n ) ) T1(n)=O(f(n)) T1(n)=O(f(n))
方法2 cal2(n) 的时间复杂度: T 1 ( n ) = O ( g ( n ) ) T1(n)=O(g(n)) T1(n)=O(g(n))
方法3 = 方法1+方法2: T ( n ) = O ( m a x ( f ( n ) , g ( n ) ) ) T(n) = O(max(f(n), g(n))) T(n)=O(max(f(n),g(n)))
3.3 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
/**
* 乘法法则:T(n) = T1(n) * T2(n) = O(n*n) = O(n2)
* @param n
*/
public static void cal4(int n){
for (int i = 0; i < n; i++) {//T1(n)
cal(n);//T2(n)
}
}
方法 cal4 循环中嵌套了 cal(n) 的执行。
T ( n ) = T 1 ( n ) ∗ T 2 ( n ) = O ( n ∗ n ) = O ( n 2 ) T(n) = T1(n) * T2(n) = O(n*n) = O(n^2) T(n)=T1(n)∗T2(n)=O(n∗n)=O(n2)
4.常见时间复杂度分析
4.1 复杂度量级
- 常量阶 O ( 1 ) O(1) O(1)
- 对数阶 O ( l o g n ) O(logn) O(logn)
- 线性阶 O ( n ) O(n) O(n)
- 线性对数阶 O ( n l o g n ) O(nlogn) O(nlogn)
- 平方阶 O ( n 2 ) 、 立 方 阶 O ( n 3 ) . . . k 次 方 阶 O ( n k ) O(n^2)、立方阶 O(n^3) ... k 次方阶 O(n^k) O(n2)、立方阶O(n3)...k次方阶O(nk)
- 指数阶 O ( 2 n ) O(2^n) O(2n)
- 阶乘阶 O ( n ! ) O(n!) O(n!)
4.2 O(1)
O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。比如这段代码,即便有 3 行,它的时间复杂度也是 O(1),而不是 O(3)。
int i = 8;
int j = 6;
int sum = i + j;
4.3 O(logn) & O(nlogn)
public static int calLogn(int n) {
int i = 1;
while (i <= n) {
i = i * 2;
}
return i;
}
每次循环 i=i*2,i 的值都会增大一倍,当 i 的值大于 n 时,结束循环
方法的执行次数 x = l o g 2 n log_2n log2n
如果将 i = i * 2 -> i = i * 3;
public static int calLogn(int n) {
int i = 1;
while (i <= n) {
i = i * 3;
}
return i;
}
方法的执行次数 x = l o g 3 n log_3n log3n
l o g 3 n = l o g 3 2 ∗ l o g 2 n log_3n = log_32 * log_2n log3n=log32∗log2n
根据前面的理论 l o g 3 2 log_32 log32 是一个常数可以忽略,所以无论 log 的底数是多少,此方法的时间复杂度都可以表示为
T ( n ) = O ( l o g n ) T(n) = O(logn) T(n)=O(logn)
假设 calLogn 方法执行 n 次,那么时间复杂度为
T ( n ) = O ( n l o g n ) T(n) = O(nlogn) T(n)=O(nlogn)
4.4 O(m+n) & O(m*n)
public static int calMN(int m, int n) {
int sum = 0;
for (int i = 0; i < m; i++) {
sum = sum + i;
}
for (int i = 0; i < n; i++) {
sum = sum + i;
}
return sum;
}
m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)
。这种情况下,加法法则修改为 T1(m) + T2(n) = O(f(m) + g(n))
,乘法法则没有影响。
5.空间复杂度分析
public static int[] calSpace(int n) {
int[] sum = new int[n];
for (int i = 0; i < n; i++) {
sum[i] = i;
}
return sum;
}
第 2 行代码 new int[n]
申请了一个大小为 n 的数组空间,其他代码占用的空间都很小,所有整段代码的空间复杂度为
O
(
n
)
O( n)
O(n),常见的空间复杂度就是
O
(
1
)
、
O
(
n
)
、
O
(
n
2
)
O(1)、O(n)、O(n^2 )
O(1)、O(n)、O(n2)。
6.小结
程序代码执行效率的瓶颈可能发生在 时间 和 空间 两个维度,但是相对于空间,时间更加宝贵;假设一段程序计算需要1G 的内存和一年时间,如果增加内存到10G,就可以缩短计算时间为半年,相信大家都会这样选择,所以大部分情况下,我们会以时间复杂度为更重要的标准。
7.参考
- 极客时间 -《数据结构与算法之美》王争