衡量你编写的算法代码的执行效率:时间&空间复杂度
1.什么是时间复杂度?
时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。
2.什么是空间复杂度?
空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
参考文章:https://www.zhihu.com/question/21387264
3.时间和空间复杂度分析的必要性
你可能会有些疑惑,我把代码跑一遍,通过统计、监控,就能得到算法执行的时间和占用的内存大小。为什么还要做时间、空间复杂度分析呢?这种分析方法能比我实实在在跑一遍得到的数据更准确吗?
事后统计法。但是,这种统计方法有非常大的局限性。
-
\01. 测试结果非常依赖测试环境
测试环境中硬件的不同会对测试结果有很大的影响。
-
\02. 测试结果受数据规模的影响很大
对同一个排序算法,待排序数据的有序度不一样,排序的执行时间就会有很大的差别。极端情况下,如果数据已经是有序的,那排序算法不需要做任何操作,执行时间就会非常短。除此之外,如果测试数据规模太小,测试结果可能无法真实地反应算法的性能。比如,对于小规模的数据排序,插入排序可能反倒会比快速排序要快!
所以,我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。这就是我们今天要讲的时间、空间复杂度分析方法。
4.大O时间复杂度
算法的执行效率,粗略地讲,就是算法代码执行的时间。但是,如何在不运行代码的情况下,用“肉眼”得到一段代码的执行时间呢?
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
从 CPU 的角度来看,这段代码的每一行都执行着类似的操作:读数据-运算-写数据。
尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以可以假设每行代码执行的时间都一样,为 unit_time。
在这个假设的基础之上,这段代码的总执行时间是多少呢?
第 2、3 行代码分别需要 1 个 unit_time 的执行时间,第 4、5 行都运行了 n 遍,所以需要 2n*unit_time 的执行时间,所以这段代码总的执行时间就是 (2n+2)*unit_time。可以看出来,所有代码的执行时间 T(n) 与每行代码的执行次数成正比。T(n)=(2n+2)*unit_time。
尽管我们不知道 unit_time 的具体值,但是通过这两段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是,所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。
T1(n)=O(f(n))
T(n) 表示代码执行的时间;
n 表示数据规模的大小;
f(n) 表示每行代码执行的次数总和。
因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
T(n) = O(2n+2),这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以
T(n)=O(2n²+2n+10000)–>O(n²)
为什么大O时间复杂度只关注代码中最复杂的循环体,这样真的准确么?万一我还有一个循环体仅次与他呢,同时另外一个代码和我比较时间复杂度时,正好和仅次与最复杂的循环体一样,这样测出来不就不准确了么?
因为n量级足够大,所以可以忽略低阶。
5.时间复杂度计算技巧
01.只关注执行次数最多的一段代码
02.加法法则:总复杂度等于量级最大的那段代码复杂度
03.乘法法则:嵌套代码的复杂度等于内外复杂度的乘积
6.常见的复杂度量级
6.1 数据量和复杂度的增长关系


我们做题的时候要想办法降低时间复杂度,这样当n的数量级上升的时候,时间复杂度的降低效果就很明显了。(比如:O(n^2)–>O(n)指数级下降)
6.2 多项式量级
多项式量级是什么?
多项量级就是说这个时间复杂度是由n作为底数的O(n) O(logn)
非多项量级就是n不是作为底数
log的详细解释?
a^x =N(a>0,且a≠1),那么数x叫做以a为底N的对数,记作x=logaN,“log”是拉丁文logarithm(对数)的缩写
log的底数是10时可以写成lg,底数是e时可以简写成ln,没有写底数的都是以10为底
6.3 非多项式量级
对于上面罗列的复杂度量级,我们可以粗略地分为两类,多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2^n) 和 O(n!)。
我们把时间复杂度为非多项式量级的算法问题叫作 NP(Non-Deterministic Polynomial,非确定多项式)问题。
当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。
7.O(1)
只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)
int i = 8;
int j = 6;
int sum = i + j;
8.O(logn),O(nlogn)
8.1 O(logn)
对数阶logn时间复杂度非常常见,同时也是最难分析的一种时间复杂度。关键在关注变量的增长情况。
i=1;
while (i <= n) {
i = i * 2;
}
从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?
实际上,变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:
所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2^x=n也就是log2n。所以,这段代码的时间复杂度就是 O(log2n)。
实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。为什么呢?
我们知道,对数之间是可以互相转换的
log3n = log32 * log2n
其中 C=log32 是一个常量。
所以 O(log3n) = O(C * log2n)
基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等于 O(log3n)。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。
8.2 O(nlogn)
如果你理解了我前面讲的 O(logn),那 O(nlogn) 就很容易理解了。还记得我们刚讲的乘法法则吗?**如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。**而且,O(nlogn) 也是一种非常常见的算法时间复杂度。
比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。
快排最好的情况下,枢轴对两边的划分都很均匀,用递归树来表示如下图:
quickSort(arr,l,p-1);
quickSort(arr,p+1,r);
9.O(m+n),O(m*n)
int cal(int m, int n) {
int sum_1 = 0;
int i = 1;
for (; i < m; ++i) {
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j = 1;
for (; j < n; ++j) {
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
从代码中可以看出,m 和 n 是表示两个数据规模。**我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。**所以,上面代码的时间复杂度就是 O(m+n)。
10.复杂度分析
10.1 对数时间复杂度&指数时间复杂度Demo

10.2 常见的搜索&排序算法时间复杂度

二分查找,二叉树遍历,二维矩阵二分查找,归并排序
01.二分查找O(logn)
一分为二,只查一边
02.二叉树遍历O(n)
思考方式01.主定理推导:每次一分为二,每边相等的时间复杂度
思考方式02.每个节点只走一次
03.排好序的二维矩阵二分查找 O(n)
一维度数组二分查找logn
有序二维矩阵 O(n),正常二维矩阵查找On^2,进行二分查找被降了一维On
04.归并排序O(n log n)
所有的排序最优都是nlogn

10.3 常用数据结构增删改查操作时空间复杂度表
10.4 计算递归的时间复杂度方法
递归计算时间复杂度,计算其语句执行次数,画出递归执行顺序的树形结构–递归状态树
斐波那契
1.首先了解总执行次数(根据执行顺序画出树形结构图–递归树/状态树)
2.每多展开一层,运行的节点数是上一层的二倍,每层节点是2^n指数级别递增,总执行次数也是指数级的且有很多重复节点

2^6
11.空间复杂度
在多数场景中,一个好的算法往往更注重的是时间复杂度的比较,而空间复杂度只要在一个合理的范围内就可以。
void print(int n) {
int i = 0;
int[] a = new int[n];
for (i; i <n; ++i) {
a[i] = i * i;
}
for (i = n-1; i >= 0; --i) {
print out a[i]
}
}
第 2 行代码中,我们申请了一个空间存储变量 i,但是它是常量阶的,跟数据规模 n 没有关系,所以我们可以忽略。
第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。
我们常见的空间复杂度就是 O(1)、O(n)、O(n^2),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。
11.1 递归的空间复杂度

为什么fib的时间复杂度是O2^n,而空间复杂度是On?
递归的空间复杂度看递归的层数
n层,空间复杂度:On
如果递归中有数组,两者之间的最大值就是你的空间复杂度
12.时间空间复杂度分析案列:60爬楼梯
递归
public static int climbStairs(int num){
if (num<2){
return 1;
}
return climbStairs(num-1)+climbStairs(num-2);
}

时间复杂度:2^n(每层都是上一层的2倍)
空间复杂度:On(递归的空间复杂度看递归的层数)
缓存数组
public static int climbStairs(int n) {
int[] memo = new int[n + 1];
memo[0]=1;
memo[1]=1;
return fib(memo,n);
}
private static int fib(int[] memo, int n) {
if (memo[n]!=0)return memo[n];
if (n>=2)memo[n]=fib(memo,n-1)+fib(memo,n-2);
return memo[n];
}
时间复杂度:On(递归不在是2^n,直接从缓存中取值)
空间复杂度:On(引入了数组)
动态规划(数组)
public static int climbStairs(int n) {
int[] arr=new int[n+1];
arr[0]=1;
arr[1]=1;
for (int i=2;i<=n;i++){
arr[i]=arr[i-1]+arr[i-2];
}
return arr[n];
}
时间复杂度:On
空间复杂度:On(引入了数组)
动态规划(无数组)
public static int climbStairs(int n) {
int first=1,second=1;
for (int i=2;i<=n;i++){
int tmp=first+second;
first=second;
second=tmp;
}
return second;
}
时间复杂度:On
空间复杂度:O1
for (int i=2;i<=n;i++){
arr[i]=arr[i-1]+arr[i-2];
}
return arr[n];
}
时间复杂度:On
空间复杂度:On(引入了数组)
### 动态规划(无数组)
```java
public static int climbStairs(int n) {
int first=1,second=1;
for (int i=2;i<=n;i++){
int tmp=first+second;
first=second;
second=tmp;
}
return second;
}
时间复杂度:On
空间复杂度:O1