前言
以自述式的笔记展示,尽可能用最好理解的方式去叙述我对知识点的理解,方便有需求的小伙伴查看理解,同时锻炼自身的表达能力,共同学习,共同进步,争取“双赢”!
注:本文章根据自身学习的笔记和自身理解编写,难免存在纰漏或不足之处,如有不足,恳请各位在评论区批评指正,谢谢!
在编写和分析算法过程中我们如何去评价一个算法的好坏程度呢?这时候,我们有时会听到“诶!我昨天晚上对那个O(N)算法进行优化,写出了一个O(logN)的算法。”等等,其中这段话中的O(N)和O(logN)便是描述算法好坏程度的一种方式-----时间复杂度和空间复杂度,而这个表示法我们通常称作“大O的渐进表示法”。接下来,我来为大家分别讲述一下时间复杂度和空间复杂度的概念、作用以及计算方式。
一、时间复杂度
1、基本概念
用于衡量算法的执行时间长短的量度
2、作用
1)事先大致估计算法的运行时间;
2)便于评价算法的好坏,且提供了一个衡量方式
3、计算规则
所谓时间复杂度,即用于计算算法运行时间的量。在实际情况下,要准确去计算一个算法的运行时间是比较麻烦的,因为一串代码的运行在不同的机器上或环境中的运行时间可能大同小异,也就是说。同一个算法的运行时间还受到软硬件等一系列的影响,但是人们为了能够描述一个算法的优劣程度,便想出这样的办法-----通过观察算法中的每行代码的执行次数进行判断。
比如以下代码的时间复杂度的判断:
double f( int n, double a[], double x ) {
int i;
double m = 1.0;
double Sum = 0;
for (i = 0; i < n; i++) {
if (i == 0) {
Sum = a[0];
} else {
m *= x;
Sum += a[i] * m;
}
}
return Sum;
}
我们观察这段代码,会发现需要执行的最多次数是1+1+1+2n+1=2n+4次,为什么呢,因为函数中前三行代码分别执行了1次,然后一个for循环执行了n~2n次,这里我们要了解一个点:就是在我们计算算法的时间复杂度的时候,一般使用的最坏情况计算,因为这样更能凸显一个算法的好坏,为什么怎么说呢?咱想想,如果用最坏的情况去考虑其时间复杂度,那么我们就会努力想这个标准减少时间,那么就算实际情况下执行了这个时间,我们的算法优化的也不错了,更何况这只是一个最坏的情况呢,换句话说,使用最坏的情况去描述一个算法的时间复杂度更加靠谱。接着return语句执行一次,一共就2n+4次了,因此这个代码的时间复杂度我们就表示成O(2n+4),意味着该穿代码时间复杂度为O(2n+4),且我们把这种表示方法称为大O的渐进表示法。
但是呢,在实际开发过程中,我们编写的算法或者程序并没有这么简单,往往代码量是特别大的,其中的方方面面混杂了不同的语句,且由于一些循环语句可能混杂了很多字母表示的执行次数(如n^2、logN等等),通常不一定只含可数的次数,就可能导致我们人为就散起来比较麻烦。因此,我们在计算时间复杂度的时候,通常只取执行次数中次数最高的、去除常系数的值作为时间复杂度的量,我们把这种评价算法执行时间长短的方式叫做算法渐进时间复杂度。比如执行n^2+3n+5次,那么我们只取n^2,所以最终的时间复杂度为O(n^2);执行6n+7次,那么我们只取n,因此时间复杂度为O(n);特殊的是:执行的是常数次,那么就取1,即最终的时间复杂度为O(1)......
那么,为什么要这样去计算呢,有什么道理?这里我说一说原因:因为当出现次数高且不能直接判断准确次数的情况(即用n或者其他字母表示的次数)时,如果n足够,那么那些低次数、为常数的值对总的执行次数的影响就显得很弱了,因此我们计算时间复杂度时只需要关注那些次数高的部分,且不用包含常系数。如果难以理解,也可以类比数学中的极限的思想,如n^2+n+5,如果n等于10或100或10^5的话,那么总的次数为10^10+10^5+5,而10^5+5相对10^10来说就显得微乎其微了,完全可以忽略不计,这样理解。因此我们计算时间复杂度时只取次数最大的部分,就是这个原因。
因此,总的来说,渐进时间复杂度的计算技巧在于:
1、找出算法中执行次数最多的位置。
2、然后分析其执行次数。
3、接着用大O表示法表示出来即可。
我们想想,我们平时编写代码时,那些地方会出现反复执行的地方呢?一般来说这种地方存在于循环语句中,即for循环或while循环中,值得注意的是,除了循环你控制语句可能出现执行次数多的情况之外,还有一个地方也经常出现,即递归,(最典型的例子阶乘或斐波那契的相关算法)。因此,我们可以在进行第一步操作时多关注这两方面。
另外,我们计算时间复杂度时,一定要具体问题具体分析,因为某些情况下,其时间复杂度会受到代码逻辑的干扰而出现多种可能的情况,这个时候我们如何确定代码执行次数呢?
就比如说,一个算法中有两个for循环,且循环次数分别为M,N,我们知道,确定执行次数时主要观察的部分之一就是循环语句,因此初步判断执行次数为M+N。但是,这个时候我们有必要留意一下,因为如果M和N的最坏情况的值的关系对实际估计是有影响的:比如,若M>>N,则此时N就可以忽略不计了,那么此时的时间复杂度就是O(M);若M接近N,则M+N可近似看作2M或2N,然后去除常系数,因此此时时间复杂度为O(M)或者O(N)。常出现这这种情况的地方在于:代码中循环语句与存在条件判断语句(if-else语句、switch语句等)相关联的时候。具体例子关注后续题型解析或自己下面练习体会。
几种常见的时间数量积的时间复杂度表示:
O(1)、O(n)、O(n^2)、O(nlogn)、O(2^n)、O(n!)
由图我们很容易看出,常数级O(1)的执行次数是最少得,也是最优原地算法,其次是O(n),最慢的是O(n!)。因此,我们在设计算法时,尽量让时间复杂度落在前几种情况最合适。
这是时间复杂度好坏排序:O(1)<O(n)<O(n^2)<O(nlogn)<O(2^n)<O(n!)
4、注意事项
在进行时间复杂度的判断时,有这么几个注意事项。
1、时间复杂度的表示
1)现在普遍使用的表示方法是大O表示法,即O(估计执行次数)。
2)执行次数的表示:对于对数的情形,因实际情况中一般式以2为底的对数,由于对数的表示时底数偏下不好表示,因此我们通常写成logN的形式表示对数级时间,即O(logN)。也可能有人表示成lgN,但不推荐,因为lg可能是以10为底,容易产生二义性。
2、执行次数值的选取
1)取代码中执行次数最多的代码块做分析即可,通常是循环控制语句和递归语句中。
2)选取的值是次数最高的,且要去除其常系数。
3)若本身是常数,则取1,相当于时间不变。
4)执行次数受代码逻辑影响(常见于循环语句与条件语句关联时)的情况,具体情况具体分析。
二、空间复杂度
1、基本概念、作用以及计算规则
空间复杂度,即用于描述一个算法占用空间的大小的一种方式。
讲完时间复杂度以后,空间复杂度就相对比较轻松了,因为空间复杂度的表示和时间复杂度几乎相同,也是用的大O的渐进表示法。只是空间复杂度表示的是一个算法在运行时所占用的临时内存,我们不会用byte去描述它的空间大小,因为单位太小,没什么太大的意义。所以空间复杂度的计算方式是去看代码中变量的个数,且其计算规则与时间复杂度的计算规则类似,我这里就不再赘述。
2、注意事项
1、空间不累计:
在循环语句中,若某个变量再循环语句中定义,那么该变量会被反复定义,可能有人会问:那这每一次定义是不是都算一个新的变量呢?答案是否定的,再循环中定义的变量总是使用的同一块空间,并不会重新开辟空间。
2、地址的开辟会利用空间一次,相当于定义了一个新变量
当涉及到地址的变量如动态内存的开辟(malloc)、递归的调用(建立栈帧)等,都会多开辟空间,因此每发生一次都相当于空间复杂度为O(1),都需要算进去。
三、总结
1、本次讲述了数据结构与算法中最基础也必不可少的概念-----时间复杂度和空间复杂度
2、时间复杂度----描述算法的执行速度,用执行次数最大的部分表示。
3、空间复杂度----描述算法占用空间的大小,用变量定义个数表示。
4、空间复杂度和时间复杂度一般用用大O的渐进表示法描述。
如有不足,请在评论区留言。