复杂度分析笔记(时间复杂度、空间复杂度)
一、什么是复杂度分析?
数据结构和算法解决的是如何让计算机更快时间、更省空间的问题。因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能。分别用时间复杂度和空间复杂度两个概念来描述性能问题,二者统称为复杂度。 复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系,所以又称渐进时间复杂度、渐进空间复杂度
二、为什么要进行复杂度分析?
- 和性能测试相比,复杂度分析有不依赖执行环境、成本低、效率高、易操作、指导性强的特点。
- 我们可以不用具体的测试数据来测试,就可以粗略地估计算法的执行效率。
- 掌握复杂度分析,将能编写出性能更优的代码,有利于降低系统开发和维护成本。
大 O 复杂度表示法
我们通常用大O复杂度表示法来表示算法的时间复杂度和空间复杂度
下面将用大 O 复杂度表示法来表示这两段代码
- 例1
int fun(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
对于上面一段代码我们可以假设每行代码执行的时间都一样,为unit_time。第 2、3 行代码分别需要 1 个 unit_time 的执行时间,第 4、5 行都运行了 n 遍,所以需要 2n*unit_time 的执行时间,所以这段代码总的执行时间(2n+2)*unit_time。可以看出来,所有代码的执行时间 T(n) 与每行代码的执行次数成正比。
- 例2
int fun(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum = sum + i * j;
}
}
}
同理上段代码总的执行时间 T(n) = (2n2+2n+3)*unit_time
尽管我们不知道 unit_time 的具体值,但是通过这两段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是,所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。
这个规律总结成一个公式就是T(n)=O(f(n))。其中T(n)表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
故上述两个例子的时间复杂度分别表示为T(n) = O(2n+2),T(n) = O(2n2+2n+3)。公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n2)。
如何分析时间复杂度?
1. 只关注循环执行次数最多的一段代码
就例1而言,其中第 2、3 行代码都是常量级的执行时间,与 n 的大小无关,所以对于复杂度并没有影响。循环执行次数最多的是第 4、5 行代码,所以这块代码要重点分析。前面我们也讲过,这两行代码被执行了 n 次,所以总的时间复杂度就是 O(n)。
int fun(int n) {
int sum = 0;
for (int i=1; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
2. 加法法则:总复杂度等于量级最大的那段代码的复杂度`
- 例3
int fun(int n) {
int sum_1 = 0;
for (int p=1; p < 100; ++p) {
sum_1 = sum_1 + p;
}
int sum_2 = 0;
for (int q=1; q < n; ++q) {
sum_2 = sum_2 + q;
}
int sum_3 = 0;
for (int i=1; i <= n; ++i) {
for (int j=1; j <= n; ++j) {
sum_3 = sum_3 + i * j;
}
}
return sum_1 + sum_2 + sum_3;
}
首先看第一段代码,这段代码循环执行了 100 次,所以是一个常量的执行时间,跟 n 的规模无关。
因为时间复杂度表示的是一个算法执行效率与数据规模增长的变化趋势,所以不管常量的执行时间多大,我们都可以忽略掉。因为它本身对增长趋势并没有影响。
第二段代码和第三段代码的时间复杂度是是 O(n) 和 O(n2)。综合这三段代码的时间复杂度,我们取其中最大的量级。所以,整段代码的时间复杂度就为 O(n2)。也就是说:总的时间复杂度就等于量级最大的那段代码的时间复杂度。
那我们将这个规律抽象成公式就是:
如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).
3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
可以把乘法法则看成是嵌套循环
类比加法法则,可以得出:如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)).
多项式时间复杂度
O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。比如:
int i=1;
int j=1;
1. O(1)
一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。
2. O(logn)、O(nlogn)
int i=1;
while (i <= n) {
i = i * 2;
}
从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。这实际上就是等比数列2x=n,x=log2n。x就是代码执行的次数。
由于对数之间是可以互相转换的,log3n 就等于 log32 * log2n,即O(Cf(n)) = O(f(n))
因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。
3. O(m+n)、O(m*n)
这一种跟前面都不一样的时间复杂度,代码的复杂度由两个数据的规模来决定。
int fun(int m, int n) {
int sum_1 = 0;
for (int i=1; i < m; ++i) {
sum_1 = sum_1 + i;
}
int sum_2 = 0;
for (int j=1; j < n; ++j) {
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。
故加法规则改为:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法则继续有效:T1(m)*T2(n) = O(f(m) * f(n))。
空间复杂度分析
1、定义:算法存储空间与数据规模增长的关系,主要看声明的空间存储变量所需要的控件存储量
2、O(1)、O(n)、O(n2),可参考基本数据类型、一维数组、二维数组
小结
1)单段代码看高频:比如循环。
2)多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。
3)嵌套代码求乘积:比如递归、多重循环等
4)多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加。