什么是大O?
若
n
表示数据规模,
算法 | 时间复杂度 | 所需指令数 |
---|---|---|
二分查找法 | O(logn) | a×logn |
数组最大/小值 | O(n) | b×n |
归并排序法 | O(nlogn) | c×nlogn |
选择排序法 | O(n2) | d×n2 |
需要注意的是,学术界定义的大O为 O(f(n)) 表示算法执行的上界,而在业界我们一般认为:
例题:
一个字符串数组,将数组中每一个字符串按照字母序排序;之后再将整个字符串按照字典序排序,时间复杂度是多少?
设字符串数组中最长字符串长度为
s
,整个有
1)每个字符串排序需要
O(slogs)
,一共
n
个字符串,也就是需要
2)对排好序的n个字符串进行字典序排序,需要比较
O(nlogn)
次,两个字符串之间进行比较需要进行
O(s)
,总体来说就需要
O(snlogn)
综上,需要的时间复杂度为:
需要注意的
算法复杂度是和用例相关,比如插入排序最差情况是 O(n2) ,最好情况是 O(n) ,业界说的时间复杂度是针对平均情况来说的,也即是 O(n2) 。
数据规模
在I7-7700HQ上测试如下代码:
#include <iostream>
#include <cmath>
#include <ctime>
using namespace std;
int main(){
for (int x = 1; x <= 9; x++){
int n = pow(10,x);
clock_t startTime = clock();
int sum = 0;
for (int i = 0; i < n; i++){
sum += i;
}
clock_t endTime = clock();
cout << "10^" << x << " : " << double(endTime - startTime) / CLOCKS_PER_SEC << " s" << endl;
}
system("pause");
return 0;
}
运行结果如下:
也就是说,如果想在1s之类解决问题:
时间复杂度 | 可以处理的数据规模 |
---|---|
O(n) | 108 |
O(nlogn) | 107 |
O(n2) | 104 |
保守估计下,可以再对数据规模除以10。
空间复杂度
总体来讲,就是多开了一个辅助的数组:
O(n)
,多开了一个辅助的二维数组:
O(n2)
;多开常数空间:
O(1)
。需要注意的是,
也就是,递归的深度是多少,所开的空间复杂度就是多少。
时间复杂度分析的一些例题
看到是双层循环,不一定都是 O(n2) 级别的算法,请看下例:
再如下例,时间复杂度为
O(nlogn)
再有,其他的一些情况:
为什么算法复杂度为
O(logn)
级别时,我们不关注
log
的底是多少?
因为不同的底之间,只是相差了一个常数,所以都是在一个量级上的,都称为 O(logn) 。
上述时间复杂度的分析的重点在于,分析与数据规模有关系的那一部分的基本操作。
递归中的时间复杂度分析
1、递归中进行一次递归调用,递归深度为depth,每个递归中时间复杂度为T,则总的时间复杂度为 O(T×depth) ,比如下面的例子:
1)在二分搜索中,递归深度为
logn
2)0到n的求和中,递归深度为 n
3)求x的n次方时,递归深度为
2、递归中进行多次递归调用,应关注递归调用的次数,可以画出一个递归树来观察
当 n=3 时,调用了
则 n 次时需要调用
均摊复杂度分析
在vector实现中,当push_back时当前动态数组容量不够时,需要重新开辟新的空间(也就是resize操作,这个过程的时间复杂度为
O(n)
),由于均摊到每一步,使得push_back操作的时间复杂度仍然是
O(1)
,也就是常数级别的,分析过程如下。
解决复杂度震荡,在pop_back操作中,当元素个数为容量的
14
时,进行resize操作,且resize操作的大小为当前容量的
12
。