直接实验得到算法时间的事后统计法以及其缺点:
事后统计法:运行代码,通过统计,监控,得到算法执行的时间和占用的内存。
事后统计法的局限性:
1、测试结果依赖于测试环境。
2、测试结果受到数据规模的影响很大。
所以我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。这就是我们今天要讲的时间、空间复杂度分析方法。
大O复杂度表示法:从cpu角度来看,每行代码都执行类似的操作:读数据-运算-写数据,尽管每行代码对应cpu执行个数,时间都不一样,但是做粗略运算,每行代码时间近似看做相等,计作unit_time。
例子:求 1,2,3…n 的累加和
int cal(int n ){
int sum = 0; //unit_time
int i = 1; //unit_time
int j = 1; //一个unit_time
for(;i<=n;i++) //n个unit_time
{
j =1 ; //n个unit_time
for(;j<=n;j++) //n个 n个unit_time
{
sum = sum + i * j ; // n个 n个unit_time
}
}
}
总的时间复杂度 T(n) = (2n² + 2n + 3) * unit_time。类似于高数求极限 所以T(n) = O(n²)。
时间复杂度分析的三个实用的方法:
1、只关心循环次数最多的一段代码 //类似于高数求极限 当n->∞,求T(n)
2、加法法则:总时间复杂度等于量级最大的那段复杂度。
代码分为三个部分,sum_1 ,sum_2,sum_3,第一段代码虽然执行了100次,但是仍然是常量执行时间,与n的规模无关。第二段第三段为O(n)和O(n²) 。还是求高数中极限的问题 。当n->∞,lim T(n) = O(n²)
3、乘法法则:嵌套代码的复杂度等于内外代码复杂度的乘积。
几种常见时间复杂度分析:
对于复杂度量级可以粗略分为两类:多项式量级和非多项式量级。非多项式用绿色波浪标注。把时间复杂度为非多项式量级的算法称作NP(Non-Deterministic polynomial)非确定多项式问题。当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解时间会无限增长。
常见的多项式量级:
1、O(1):代码执行时间不随着n的增大而增大,都称作常数量级的时间复杂度。
2、O(logn)、O(nlogn)
int i = 1;
while(i <= n)
{
i = i *2 ;
}
变量i的值从1开始取,每循环一次乘以2,取值过程为一个等比数列:
当第x次,恰好大于n,所以x = log2n,时间复杂度O(log2n)
但是实际上,不管是以几为底( i = i * 3 ;//以三为底, i = i * 10 ;//以十为底),都可以把所有对数阶时间复杂度记作O(logn)
因为对数之间是可以相互转换的,log3n等于log3 2 * log 2 n 所以相差一个常量 C = log 3 2 ,可以忽略。所以在对数阶时间复杂度表达里面,忽略对数的“底”,统一记作O(log n),所谓O(nlogn) 就是把时间复杂度O(logn)的代码执行n次,如归并排序和快速排序。
3、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)。
时间复杂度全称:渐进时间复杂度,表示算法的执行时间和数据规模之间的增长关系。
空间复杂度全称:渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
课后问答:我们上算法课,老师讲到存储一个二进制数,输入规模(空间复杂度)是O(logn) bit。请问如何理解?
回答:比如数据规模为8,8个数字,用2进制的表示的话,只用3位就可以表示出0~7八个数字;数据规模为16,用2进制表示的话,只用4位就可以表示出来0~15个数字。
四个复杂度分析方面的知识点:
best case time complexity:O(1) worst case time complexity:O(n)
情形:在长度为n的数组中查找某个数。
int find(int[] array, int n, int x) {
int i = 0;
int pos = -1;
for (; i < n; ++i) {
if (array[i] == x) pos = i;
}
return pos;
}
average case time complexity:
查找变量x在数组中的位置,有n+1种情况:在数组的0~n-1位置中和不在数组中,考虑每种情况,然后再除以情况n + 1种情况数。
根据时间复杂度的大O标记法,省去系数,常量,低阶,简化后为:O(n)。
但是实际情况并不是这样,数组的0~n-1位置中和不在数组中发生的概率是不同的。假设在数组中和不在数组中的概率1/2,另外查找数据出现在数组的0~n-1位置中任意位置的概率就是1/(2n),所以平均时间复杂度:
该值叫做概率论中加权平均值,也叫期望值,所以平均时间复杂度全称:加权平均时间复杂度或者期望时间复杂度。
根据时间复杂度的大O标记法,省去系数,常量,低阶,简化后为:O(n)。
amortized time complexity 对应的分析方法:摊还分析(平摊分析)
平均时间复杂度只在某些特殊情况会用到,而均摊时间复杂度的应用场景更加特殊和有限。
数组的插入:如果数组满了,则遍历数组,把所有累加和存放到第一个位置;否则直接插入。
时间复杂度O(n)和O(1)。每一次O(n)的操作后面都会跟着n-1次O(1)的操作,所以把耗时多的那次操作均摊到接下来n-1次耗时少的操作,均摊下来,这一组连续操作的均摊时间复杂度就是O(1)。
然而什么时候使用均摊时间复杂度呢?
对于一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间满足前后连续的时序关系,这样就可以把这一组操作放在一起分析,看是否能把较高的时间复杂度的耗时,平摊到其他时间复杂度比较低的操作上,并且能够使用均摊时间复杂度分析的场合,一般均摊时间复杂度等于最好情况时间复杂度。
tips:重要的不是区别平均时间复杂度和均摊时间复杂度,而是掌握均摊分析。
课后例题:
// 全局变量,大小为10的数组array,长度len,下标i。
int array[] = new int[10];
int len = 10;
int i = 0;
// 往数组中添加一个元素
void add(int element) {
if (i >= len) { // 数组空间不够了
// 重新申请一个2倍大小的数组空间
int new_array[] = new int[len*2];
// 把原来array数组中的数据依次copy到new_array
for (int j = 0; j < len; ++j) {
new_array[j] = array[j];
}
// new_array复制给array,array现在大小就是2倍len了
array = new_array;
len = 2 * len;
}
// 将element放到下标为i的位置,下标i加一
array[i] = element;
++i;
}
最好时间复杂度:O(1)
最差时间复杂度:O(n)
平均时间复杂度:
如果用平均时间复杂度分析
(1+1+...+1+n) / (n+1) = 2n/(n+1) = O(1)
如果是加权平均时间复杂度分析
1*(1/n+1) + 1*(1/n+1) + ... + 1*(1/n+1) + n * (1/n+1) = O(1)
如果是使用均摊法
前面n个操作的时间复杂度都是O(1),第n+1次操作的时间复杂度是O(n),所以均摊到前面n次,均摊下来时间复杂度为O(1)