引言
谈到数据结构与算法,就一定离不开时间空间复杂度分析。复杂度分析是整个算法学习过程中的精髓,只有学会了复杂度分析,我们才能更好的分析各个数据结构与算法的优劣。在不同的场景下选择适合的数据结构与算法,从而提高代码的执行效率。
一.为什么要进行复杂度分析?
我们可以将代码跑一遍,通过统计、监控,进行性能测试就能得到算法执行时间和占用的内存空间。为什么还要做时间、空间复杂度分析呢?这是因为性能测试依赖于测试环境与数据量。所以我们需要一种无关测试环境与数据量的方法来粗略的估算算法的执行效率。我们把这种方法称为复杂度分析。
小结:
- 与性能测试相比,复杂度分析不依赖执行环境、成本低、效率高、易操作、指导性强的特点。
- 掌握复杂度分析,可以编写出性能更优的代码,有利于开发和维护成本。
二.什么是复杂度分析?
数据结构与算法
数据与结构与算法的宗旨就是“如何让计算机更快时间、更省空间的解决问题”,所以从执行时间、占用空间两个维度来评估数据结构与算法的性能。
复杂度
我们分别用时间复杂度、空间复杂度两个概念来描述性能问题,将两者统称为复杂度。复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系。
小结:
- 复杂度分析分为时间复杂度与空间复杂度。
- 时间复杂度是为了解决“快”的问题,让代码运行的更快;
- 空间复杂度是为了解决“省”的问题,让代码更省存储空间。
三.如何进行复杂度分析?
1.大O表示法
1. int cal(int n) {
2. int sum = 0;
3. for (int i = 1; i <= n; ++i) {
4. sum = sum + i;
5. }
6. return sum;
7. }
从上面可以看出第3、4行分别执行n次,其他行执行一次。
假设每行代码的执行时间都一致为once_time,我们每行代码的执行次数为n*once_time,这段代码的执行时间为T(n)=(第一行执行次数 * once_time + 第二行执行次数 * once_time +...+最后一行执行次数 * once_time)。我们称将每行的执行次数用f(n)函数表示。所有的代码执行时间T(n)与每行代码的执行次数f(n)成正比。
由此,我们可以得出一个公式:
T(n) = O(f(n))
其中T(n)代表代码的执行时间;n代表的是数据规模的大小;f(n)代表的是每行代码执行次数的总和;公式中的O代表代码的执行时间T(n)与执行次数f(n)成正比。
如T(n)=O(2n+2),T(n)=O(n² + 2n + 3)...,这就是大O时间表示法。大O时间复杂度实际上并不代表代码真正的执行时间,而是代表代码执行时间随着数据规模增大的变化趋势,所以,也叫渐进时间复杂度,简称时间复杂度。对于空间复杂度也是一样的,代表代码的占用空间随着数据规模增大的变化趋势。
2.时间复杂度分析
-
单段代码看高频:比如循环。
-
多段代码取最大:比如一段代码中即含有单循环、也有双循环,那么取双循环的复杂度。
-
嵌套代码求乘积:比如递归。
-
多个规模求加法:比如方法中有两个参数控制两个循环的次数,那么这时就分别计算时间复杂度并取相加。
//1.单段代码看高频:
//第三/四行执行n次,其它行数只执行一次。第三四行属于高频,忽略其它行,计算时间复杂度为O(n)。
1. int cal(int n) {
2. int sum = 0;
3. for (int i = 1; i <= n; ++i) { //只需要看
4. sum = sum + i;
5. }
6. return sum;
7. }
//2.多段代码取最大:
//第4-6行是单循环,第10-14行是双循环。忽略单循环,计算时间复杂度为O(n²)
1. void cal(int n) {
2. int sum = 0;
3. int i = 1;
4. for (; i <= n; ++i) {
5. sum += i;
6. }
7.
8. int i = 1;
9. int j = 1;
10. for (; i <= n; ++i) {
11. for (; j <= n; ++j) {
12. sum1 += j;
13. }
14. }
15.
16. }
//3.嵌套代码求乘积:
//在cal函数中第一个循环T1(n) = O(n)。第一个循环内调用了cal2函数,在cal2函数中的循环T2(n)=O(n)。计算时间复杂度为T(n) = T1(n)* T2(n) = O(n)*O(n) = O(n²)
1. int cal(int n) {
2. int sum = 0;
3. for (int i = 1; i < n; ++i) {
4. sum = sum + cal2(i);
5. }
6. }
7.
8. int cal2(int n) {
9. int sum = 0;
10. for (int i = 1; i < n; ++i) {
11. sum = sum + i;
12. }
13. return sum;
14. }
//4.多个规模求加法:
//在cal函数中第一个循环T1(n) = O(n)。第二个循环T2(n)=O(m)。计算时间复杂度为T(n) = T1(n)+ T2(m) = O(n) + O(m)
1. int cal(int n, int m) {
2. int sum = 0;
3. for (int i = 1; i < n; ++i) {
4. sum = sum + i;
5. }
6. int sum1 = 0;
7. for (int j = 1; j < m; ++m) {
8. sum1 = sum1 + j;
9. }
10. }
四、空间复杂度
类似时间复杂度分析,只是将执行时间变为了占用空间。这里就不做介绍了。
五、复杂度级别
时间复杂度按照量级一般分为以下几种:
O(1)、O(logn)、O(n)、O(nlogn)、O(n²)、O(2ⁿ)、O(n!)
多项式阶:随着数据规模的增长,算法的执行时间与空间占用,按照多项式的比例增长。如 O(1)、O(logn)、O(n)、O(nlogn)、O(n²)。
非多项式阶:随着数据规模的增长,其执行时间与占用空间暴增,这种算法是极差的。如 O(2ⁿ)、O(n!)。