算法学习系列1——时间、空间复杂度分析
Big O notation(大O符号表示法)
O(1): Constant Complexity 常数复杂度;
O(log n ): Logarithmic Complexity 对数复杂度;
O(n): Linear Complexity 线性时间复杂度;
O(n²): N square Complexity 平方复杂度;
O(n³): N cubic Complexity 立方复杂度;
O(2n): Exponential Complexity 指数复杂度;
O(n!): space 阶乘复杂度;
对于复杂的的计算,只看最高复杂的运算;
如何计算时间复杂度
1.最常用的方式就是直接看函数,分析这段代码运行了多少次;
2.不考虑代码运行时带来的系数差异;
eg.
//O(1) 不论n为多好,只执行一次println
int n = 2000;
System.out.println("Hello-input:" + n);
//O(1) 不论n为多少,只执行3次println,切记没有O(3)的表示法
int n = 2000;
System.out.println("Hello-input1:" + n);
System.out.println("Hello-input2:" + n);
System.out.println("Hello-input3:" + n);
以上两段代码均是*O(1)*复杂度;
eg.
//O(n) 执行次数与输入n满足:(执行次数)=n的线性关系
for (int i = 1; i <= n; i++) {
System.out.println("Hello-input4:" + i);
}
//O(n²) 嵌套循环中则执行次数与输入n满足:(执行次数)=n²的线性关系
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
System.out.println("Hello-input5:" + i "and" + j);
}
}
上述代码第一段与n成线性关系,即O(n),第二段与n成平方关系,即O(n²);
eg.
//O(log(n)) 执行次数与输入n满足:(执行次数)=log(n)的对数关系
for (int i = 1; i <= n; i = i * 2) {
System.out.println("Hello-input6:" + i);
}
//O(k^n) 递归方法中执行次数与输入n满足:(执行次数)=k^n的指数关系
int fibo(int n) {
if (n < 2) return n;
return fibo(n - 1) + fibo(n - 2);
}
上述代码第一段与n成对数关系,即O(log(n)),第二段递归为指数关系,即O(kn);
n小于10时几种复杂度差异不大,随着运行次数增大,指数、阶乘等复杂度会变得十分恐怖,所以注意算法的时间及空间复杂度,是非常有必要的,此处就不在附上时间复杂度比较图表了,随便一搜索就可以找到;
递归条件下如何计算时间复杂度
求第n个斐波那契数列
F(n) = F(n-1) + F(n-2)
int fibo(int n) {
if (n < 2) return n;
return fibo(n - 1) + fibo(n - 2);
}
比如如果计算n=6的话,就要计算n=5与n=4,而n=5需要计算n=4和n=3,而n=4需要计算n=3和n=2,以此类推,最终都需要计算到n=1与n=0,就是说最后运算的状态树为2n,复杂度就是指数层级的。
如果想要降低递归算法的复杂度可以可以加缓存,存储中间项(因为整个状态树中有较多重复计算项),或者直接采用循环方法;
常用的递归复杂度计算公式
- 二分查找 Binary search
因为每次只计算一边,所以时间复杂度可以看为O(log n); - 二叉树遍历 Binary tree traversal
可以这么来看二叉树的节点每个都要访问,且仅仅访问一次,所以时间复杂度为O(n); - 有序二维矩阵二分查找 Optimal sorted matrix
一维数组的二分查找是O(logn),有序二维矩阵二分查找的时间复杂度为O(n); - 归并排序 Merge sort
时间复杂度为O(n log n),且排序算法的复杂度最优为O(n log n);
上述复杂度均可通过Master Theory证明得出,比较复杂,有兴趣者可以自行查阅;
思考
- 二叉树遍历-前序、中序、后序遍历时间复杂度多少?
看二叉树的节点每个都要访问,且仅仅访问一次,所以时间复杂度为O(n); - 图的遍历:时间复杂度是多少?
图的节点只访问一次,复杂度为O(n); - DFS、BFS时间复杂度是多少?
*搜索空间里的节点只访问一次,复杂度为O(n);
空间复杂度
如何计算空间复杂度
计算空间复杂度主要考虑以下两点:
- 程序中数组的长度
- 程序中递归的深度
如果代码中开辟的空间为数组,那你的空间复杂度通常就是数组的长度;
如果开启了递归,那么代码的空间复杂度就是递归的深度;
如果二者兼得,那么选择二者的最大值;
eg.
//本代码仅为示例讲解,并不保证可以完美运行
class Body{
public String getSpace(int n, int m) {
int[] space= new int[n];
space[0] = 0;
for (int i = 1; i < n; ++i) {
space[i] = space[i - 1] * i;
}
for (int i = 1; i <= n; ++i) {
int funder= m / space[n - i] + 2;
for (int j = 1; j <= n; ++j) {
funder += valid[j];
int[] ant = new int[n];
if (funder == 0) {
ant.append(j);
valid[j] = 0;
break;
}
}
m %= space[n - i];
}
return ant.toString();
}
}
上述代码,运行次数做多,时间复杂度最高的部分就是嵌套循环,运行次数为n2次,那么该段程序的时间复杂即为O(n2);
空间复杂度:可以看到代码开始部分实例化了一个数组 int[] space = new int[n] ,且程序内部没有递归发生,那么该段程序空间复杂度为数组长度,即为O(n);