前言
衡量代码是否牛逼有两个非常重要的指标:
1、运行时间;2、占用空间。
数据结构和算法本质上是解决“快”和“省”(程序运行速度快,存储空间省)的问题,因此算法的执行效率尤其重要,那么如何来衡量代码的执行效率尼?时间、空间复杂度分析它就lei了。
数学题(自编)
例1:春天来了,万物复苏,又到了。。。植树的时候了,给小萌10棵树,小萌种一棵树需要半天(0.5天),那么全部种完需要几天?
答案:10X0.5=5(天)
那么给小萌n棵树尼?0.5n(天)
-----------------------我是分割线-------------------
例2:柠檬树上柠檬果,而柠檬树下只有小萌,小萌数了下树上有8(好少)个果子,他准备每3天摘下树上一半的果子,问几天后树上只有1个果子?
分析:每次摘一半,就是不断的除以2,这里就需要用到对数(以2为底,8的对数)简写log8
答案:3Xlog8=3X3=9(天)
那么树上如果有n个柠檬果尼?3logn(天)
-----------------------我是分割线-------------------
例3:疫情期间学校延迟开学,小萌在家期间写了5篇作文,他写第一篇时用了1天,写第二篇用了2天,第三篇用了3天。。。每多写一篇就多花1天时间(可能他妈妈已经三天没打他了),问小萌5篇作文一共写了几天?
答案:1+2+3+4+5=15(天),其实就是1到5的累加
那么如果写n篇需要多少天尼?1+2+3+n-1+n=n(n+1)/2
渐进时间复杂度/大O时间复杂度表示法
通过上面的数学题我们可以总结出一个规律:小萌做事情所需要的时间是和事情多少是成正比的。可以把这个规律用一个公式表示:
Tn=O(f(n))
这就是大O时间复杂度表示法,T(n)就是代码执行时间,n表示数据规模,f(n)表示代码执行次数总和,O表示代码的执行时间 T(n) 与代码执行次数总和 f(n) 表达式成正比。
把上面的数学题答案用大O复杂度表示法改写下就是下面这样:
例1:T(n)=O(0.5n);例2:T(n)=O(3logn);例3:T(n)=O(0.5n^2+0.5n);
大O时间复杂度表示法实际上不能表示程序真正的运行时间,它只表示代码执行时间随数据规模增长的变化趋势,所以也叫渐进时间复杂度。
Talking is cheap,show me your code!(不BB,看代码!)
来一起分析下这段代码的时间复杂度,例4:
int test(int n){
int sum =0;
int x =1;
int y =1;
for(;x<=n;x++){
y=1;
for(;y<=n;y++){
sum=sum+x+y;
}
}
return sum;
}
分析:假设每行代码执行需要exe_time时间,2-4行代码只执行一次,需要3*exe_time,5和6行代码都循环执行了n遍,需要2n*exe_time,6和7行都循环执行了n^2遍,需要2n^2*exe_time,整段代码所需执行时间3*exe_time+2n*exe_time+2n^2*exe_time,用大O复杂度表示法:T(n)=O(2n^2+2n+3)
时间复杂度分析方法
前面说过大O时间复杂度只表示代码执行时间的一个增长趋势,假设n无限大的时候,从数学的角度去分析下上面例子的公式会发现其实还是可以继续简化的,改写后如下:
例1:T(n)=O(n);例2:T(n)=O(logn);例3:T(n)=O(n^2);例4:T(n)=O(n^2);
大致的曲线图如下:
把常量、系数和低阶的都去掉,因为这些并不影响增长的趋势。我们在实际开发过程中一个方法的代码可能几十行甚至上百行,所有代码进行分析显然不科学也很无聊,下面总结出几个常见的时间复杂度分析方法:
方法一、只关注循环次数最多的代码
根据关键词for或者while去分析代码
方法二、取量级最大的那段代码的时间复杂度
我们看例3的公式T(n)=O(0.5n^2+0.5n),观察到量级最大的就是n^2,所以简化后就是T(n)=O(n^2)
方法三、嵌套代码的复杂度取嵌套内外代码的乘积
比如方法A中有个循环内部调用了方法B,方法B中也有一个循环,那么方法A的时间复杂度就是方法A和方法B的乘积
几种常见的时间复杂度量级
前面我们已经接触过T(n)=O(n),T(n)=O(logn),T(n)=O(n^2)这几种了时间复杂度量级,其他常见的还有
T(n)=O(1),T(n)=O(nlogn),T(n)=O(n!),T(n)=O(2^n)
两个特别说明的例子:
O(1),这个并不代表只执行1行代码,只是代码执行是常量级的,只要没有循环递归即使有上千行代码,其时间复杂度还是O(1)
O(m)+O(n),当方法中数据规模不止一个时,方法二就不适用了,因为没办法评估哪个量级大,所以需要算在一起。
开放型数学题(自编)
例1:小萌和小明在做一个游戏,小明口袋里有6个不同颜色的油画棒,小明指定一个6种颜色之一让小萌从他口袋里拿,每次只能拿出1个,问小萌需要拿几次才能拿到小明说的那个颜色的油画棒?
A.1次 B.6次 C.1~6次都可能 D.3次
分析:不知道选啥时三短一长选最长(我做学习强国挑战答题时就这思路),其实这题很明显选C。
简单描述下就是小萌运气爆炸第一次就拿到,或者小萌点数太背最后一个才拿到,那么介于运气爆炸和点数太背之间2345次都有可能。
延伸下:小萌和小明一直重复这个游戏,问小萌平均几次能拿到尼?
分析:小萌会在6种情况下拿到(1次2次...6次),把每一种情况下的次数加起来除以6就是一个平均次数,(1+2+3+4+5+6)/6=3.5(次)
那么如果小明口袋有n个不同颜色的油画棒尼?那就是1~n次,平均次数就是
最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度
其实我们把上面这个例子类比到时间复杂度上就是最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度,用大O表示法:最好情况时间复杂度O(1),最坏情况时间复杂度O(n),平均情况时间复杂度(去掉常量和系数)O(n)。
我们回过头来看上面那个题目,算平均次数的过程是不是存在问题?什么问题?我们是不是忘了考虑每种情况的概率,感性上我们是不是认为运气爆炸和点数太背这两种情况出现的概率会相对低一些?理性上这6种情况的概率是一样的,都是1/n。我们把概率带入公式再算一次就变成
这个值就是概率论中的加权平均值,所以平均情况时间复杂度也叫加权平均时间复杂度。
这三种复杂度只有同一段代码在不同情况下,时间复杂度出现量级差距时我们才会用。
均摊时间复杂度
至此我们已经掌握了大部分时间复杂度的分析,最后我们再讲一个好玩的时间复杂度叫均摊时间复杂度,是不是感觉跟平均时间复杂度有点像?
我们试着用最好最坏和平均时间复杂度来分析下这段代码,最好情况,插入数据时数组没满,执行16和17行代码,复杂度O(1),最坏情况,插入数据时正好数组满了,执行8-17行代码,复杂度O(n)。
这段代码执行会出现n+1种情况,其中有n种情况下只执行一次,只有一种情况下执行是n次,算式:
我们来对比下上面查找和插入的两个例子会发现两个特点:
1、查找时只有在最好的情况下复杂度才是O(1),其余都是O(n),而插入时只有在最坏的情况下才是O(n),其余都是O(1)
2、插入操作是不是有一定的规律?当最坏的情况出现后是不是接下来就是n-1种最好的情况,这样依次重复着?
针对这种场景,我们用一个更简单的分析方法叫摊还分析法,通过该方法得出的复杂度我们叫均摊时间复杂度,我们把那1次的最坏情况均摊到n-1次最好的情况,那么时间复杂度就是O(1)了。
在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。
总结
作为程序员写代码时不能够只想着代码能work就ok了,要养成代码时间空间复杂度分析的习惯,尽可能地让代码最优最高效。习惯的力量是惊人的。
来源:oschina
链接:https://my.oschina.net/itliang/blog/3211541