what:复杂度分析是什么?
复杂度分析,也叫渐进式负责度分析,是评估解决某问题的算法,随着该问题数据规模的增长,其所耗费的时间、所占用的空间(内存)增长的趋势
why:为什么需要复杂度分析?
我们有其他方法评估一个算法的复杂度,比如:针对某算法,在机器上,实现运行,然后统计它的时间耗费、空间占用。但有缺点如下:
- 不同机器的性能不同,比如好算法在老机器的执行效率,很可能根本比不过坏算法在新macbook pro上的效率
- 无法测试覆盖到所有的数据规模(不可能测试巨量的数据,来验证一个算法的复杂度吧,这不划算,老板会fire you)
how表示:大O表示法
那么:如何表示一个算法的复杂度呢?
答:用大O表示法,O(f(n)) 这里的f(n)是一个数据函数,n是表示数据的规模,是自变量,常见的fn有:y=100(100表示一个常量)表示y与n规模无关 y=logn 表示y随着n呈现出对数性增长 y=kn 表示y对n线性增长 ;y= nlogn y=n^2 y=n^3 y=n^3... y=n! y=2^n
时间复杂度
常数阶O(1)
1代指一个常数,表示算法和n的规模无关,是固定的
对数阶O(logn)
O(logn)表示,算法用时,随着n规模的增长,呈现出对数性增长,对数型增长的特点:n规模越大,其增长速度是变慢的,画图表示,其斜率是降低的
为什么对数没有指定具体的底
因为: 假设底为10 那么:log10 n = log10 2 * log2 n(不会输入底的数字,尴尬)其他log10 2为常数,常数又可以忽略,那么所有的对数阶就都可以表示成,忽略所有对数的底 O(logn)了
线性阶O(n)
O(n)表示:算法用时,随着n规模增长,斜率是固定的,
线性对数阶O(nlogn)
O(nlogn)为O(n)* O(logn) ,增长比O(n)要快,一般是一个n规模的循环,加内部是一个递归(递归的复杂度常常为对数阶O(logn)) 比如:
for (j=1; j<=n; j++) {
i=1;
while (i <= n) {
i = i * 2;
}
}
k次方阶 O(n^k)
k可能为2、3、... k
指数阶O(2^n) 和 阶乘阶O(n!)
指数阶2^2 和阶乘阶 n! 会随着n的增长,而极具增长,这样复杂度的算法,一般不考虑,非常低效!这俩也被称为非多项式量级(其他的叫做多项式量级),也叫NP问题(non-deterministic polynomial)
4种时情况下的:时间复杂度
why:为什么会要分析这4种情况下的时间复杂度
所谓情况:指的的算法真正执行时,所要操作的数据、及一些输入参数的实际情况,假设:我有一个查找算法,就是遍历一个无序数组,找到100这个数字,找到后,随机返回其数组下标;那么:同样的算法(遍历)在不同的情况下,其某次实现执行时的复杂度就有所不同了,比如100在第一个位置,那就是O(1),在最后一个位置,那就是O(n);所以就有了下面几种情况下的时间复杂度分析...
一般情况下,不需要计算这几种情况下的时间复杂度,只有在:同一段算法,在不同输入的情况,导致的复杂度量级差异过大的情况下,才需要进行分析!
// n表示数组array的长度
int find(int[] array, int n, int x) {
int i = 0;
int pos = -1;
for (; i < n; ++i) {
if (array[i] == x) {
pos = i;
break;
}
}
return pos;
}
最好时间复杂度
当查找的100,在第一个位置,很lucky,此时就是最好情况,该算法最好时间复杂度为O(1)
最坏时间复杂度
当查找的100,在最后一个位置,或根本不在,此时就是最坏情况,最好时间复杂度为o(n)
平均情况时间复杂度
考虑到100在数组中的情况,有2大类情况,在和不在(这里先近似理解为各1/2,概率论的先忽略),那么在的情况下,其出现位置又有从0 到 n-1共计n种情况,然后:计算每种情况的时间复杂度,再各种乘上各种情况下出现的概率,(平均情况时间复杂度,应该叫加权平均时间复杂度,也叫期望时间复杂度——要考虑各种情况下的概率)
该算法的平均时间复杂度为:
为:O(n)
均摊时间复杂度
也叫摊还,平摊时间分析,看下面的例子
根据count情况的不同,我们来计算均摊时间复杂度 ,count可能为0 到 n - 1,此n种情况,复杂度都是O(1),count如果等于数组长度,那么此时复杂度为O(n)(遍历数组),那么复杂度计算为:
// array表示一个长度为n的数组
// 代码中的array.length就等于n
int[] array = new int[n];
int count = 0;
void insert(int val) {
if (count == array.length) {
int sum = 0;
for (int i = 0; i < array.length; ++i) {
sum = sum + array[i];
}
array[0] = sum;
count = 1;
}
array[count] = val;
++count;
}
均摊时间复杂度,和平均时间复杂度的区别?
比较insert()和find()发现:
- find()是极端情况下,复杂度才为O(1),而insert大部分都是O(1)
- insert的规律性:一个O(n)为,跟着n-1个O(1),如此循环(这里假设外部有个循环,一直让count从0到10循重复),
- 总结:摊还分析的应用场景,对一个数据结构,进行连续操作,大部分情况下复杂度都低,但是连续操作会让数据本身(产生一定趋势,比如这里的数据一直插入直到满),到达某个“势”的点后,复杂度会突然增大,这样就可以利用摊还时间分析,把复杂度高的那次,均摊到其他复杂度低的时候。
空间复杂度
这里申请了一个n大小的数组,所以空间复杂度为n
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]
}
}
复杂度计算的法则
当一个算法中,包括多部分代码,那么根据多个部分代码的组合形式,一般使用加法法则、乘法法则
基本原则
算法复杂度分析时,只取最高阶的部分,且可以忽略常数部分
加法法则
多见于:各部分代码是,串行先后顺序执行的情况下,那么该算法的复杂度,取决于阶数最高的那部分,比如f(n)= 100 + n + n^2 那么这段代码的时间复杂度就为n^2 忽略其他低阶部分
注意:如果一个算法取决于2个数据的规模,算法整体复杂度,就是2个数据各自算法复杂度的相加了,(因为不能确定,2个数据的实际规模,所以不能忽略) 比如如下代码复杂度就是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;
}
乘法法则
多见于循环嵌套的代码,比如下面算法的复杂度就是O(n*n) = O(n^2):
int cal(int n) {
int ret = 0;
int i = 1;
for (; i < n; ++i) {
ret = ret + f(i);
}
}
int f(int n) {
int sum = 0;
int i = 1;
for (; i < n; ++i) {
sum = sum + i;
}
return sum;
}