这两天看了一下《大话数据结构》中的算法时间复杂度,感觉还是挺详细,对算法是时间复杂度有了一个更深刻的了解,为了加深对算法的时间复杂度的记忆和理解,同时也和大家分享我对算法时间复杂度的理解,如果有问题,希望能够多多指点交流。
函数的渐进增长
现在有两个算法:算法A(2n+3)和算法B(3n+1),你觉得这两个算法那个更快一些呢?
准备来说,答案是不一定的(如下表)
次数 | 算法A(2n+3) | 算法A‘(2n) | 算法B(3n+1) | 算法B’(3n) |
---|---|---|---|---|
n=1 | 5 | 2 | 4 | 3 |
n=2 | 7 | 4 | 7 | 6 |
n=3 | 9 | 6 | 10 | 9 |
n=10 | 23 | 20 | 31 | 30 |
n=100 | 203 | 200 | 301 | 300 |
在n=1时,算法B效率更高,因为算法B次数更少。当n=2时,两者效率相同;当n>2时,算法A就开始优于算法B,并且随着n的增加,算法A效率越来越高(执行的次数比B要少)。因此这时算法A比算法B要更好。
因此在输入规模n在没有限制的情况下,只要超过一个数值N,这个函数就总是大于另一个函数,我们称函数是渐进增长1的。同时可以发现后面的+3和+1其实对算法的影响并不大,因此,我们可以忽略这些加法常数。
同样,请看算法C(4n+8)和算法D(2n2+1),你觉得哪个算法更好呢?
次数 | 算法C(4n+8) | 算法C’(2n) | 算法D(2n2+1) | 算法D’(2n2) |
---|---|---|---|---|
n=1 | 12 | 1 | 3 | 1 |
n=2 | 16 | 2 | 9 | 4 |
n=3 | 20 | 3 | 19 | 9 |
n=10 | 48 | 10 | 201 | 100 |
n=100 | 408 | 100 | 20001 | 1000 |
当n<=3时算法C的没有D好,因为C次数比较多,但当n>3,算法C的优势就越来越明显,n越大两算法的差距则越大。而当去掉后面的常数后,其实对结果也没有改变。就算去掉与n相乘的常数,结果也没有发生改变,C‘的次数随着n的增长,还是远小于算法D’。因此,与最高次项相乘的常数并不重要。
第三个例子。算法E(2n2+3n+1),算法F(2n3+3n+1)。
次数 | 算法E(2n2+3n+1) | 算法E‘(n2) | 算法F(2n3+3n+1) | 算法F’(n3) |
---|---|---|---|---|
n=1 | 6 | 1 | 6 | 1 |
n=2 | 15 | 4 | 23 | 8 |
n=3 | 28 | 9 | 64 | 27 |
n=10 | 231 | 100 | 2031 | 1000 |
n=100 | 20301 | 1000 | 2000301 | 1000000 |
n=1时两个算法的结果相同,当n>1后,算法E的优势就要开始优于算法F,且随着n的增大差距非常明显。通过观察发现,最高次项的指数越大,函数随着n的增长,结果也会越来越大。
最后一个例子。算法G(2n2),算法H(3n+1),算法I(2n2+3n+1)
次数 | 算法G(2n2) | 算法H(3n+1) | 算法I(2n2+3n+1) |
---|---|---|---|
n=1 | 2 | 4 | 6 |
n=2 | 8 | 7 | 15 |
n=3 | 50 | 16 | 66 |
n=10 | 200 | 31 | 231 |
n=100 | 20000 | 301 | 20301 |
n=1000 | 2000000 | 3001 | 200301 |
n=10000 | 200000000 | 30001 | 20003001 |
通过这三个算法可以清楚的看到。当n的值越来越大时,3n+1已经和另外两个算法结果相比较了,最终机会可以忽略不计。也就是说,随着n值变的非常大以后算法G其实已经很趋近与算法I。于是我们可以得到这样的一个结论——判断一个算法的效率时,函数中的常数和其它次项要项常常可以忽略,而更应该关注最高次项的项数。
因此判断一个算法效率时,不能只通过少数数据来判断。通过上面的例子我们可以发现,我们可以通过对比这几个算法的关键执行次数函数的渐近增长性,来得出一个算法随着n的增大,它会越来越优于另一个算法,或者越来越劣于另一个算法。通过算法的时间复杂度来估算一个算法的时间效率。
算法时间复杂度
算法时间复杂度定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n) =O(f(n))。它代表随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
一般情况下,随着n的增大,T(n)的增长最慢的算法为最优算法。
显然,由此算法时间复杂度的定义可知,从上面几个例子中,用到了常数阶O(1)、线性阶O(n)、平方阶(n2),当然还有一些其他的阶,后面会和大家列举。
-
推导大O阶的方法
- 推导大O阶
-
用常数1取代运行时间中的所有加法常数。
-
在修改后的运行次数函数中,只保留最高阶项。
-
如果最高阶项存在且不是1,则取出与这个项相乘的常数。
得到的结果就是大O阶,之所以用这样得出的原理,从上面的函数渐进增长就能总结出。
-
常见的时间复杂度
-
常数阶
int sum =0,n=100; /*执行一次*/ sun = (1+n)*n/2; /*执行一次*/ printf("%d",sum); /*执行一次*/
这个算法的运行次数函数是f(n)=3。根据上面推导方法,将常数项3改为1。因为没有最高阶项,因此这个算法的时间复杂度为O(1)。
同时可以发现无论n为多少,代码之间的差异仅仅只是执行次数的差异。这种与问题大小无关(n的多少),执行时间恒定的算法,称为O(1)时间复杂度,又叫常数阶。
无论常数是多少,都记作O(1),而非O(3)、O(7)等其它数字。而且单纯的分支结构(不包含循环结构中),其时间复杂度也是O(1)。
-
线性阶
要确定某个算法的阶次,常常需要确定某个特点语句或语句集运行的次数。因此,我们要分析算法的复杂度,关键就是要分析结构的运行情况。
int i; for (i = 0;i<n;i++){ /*时间复杂度为O(1)的程序步骤序列*/ }
因为循环体中的代码要执行n次。所以实践复杂度为O(n)。
-
对数阶
int count = 1; while (count < n){ count = count * 2; /*时间复杂度为O(1)的程序步骤序列*/ }
由于每次count乘以2之后,就距离n更近了一分。也就是说,有多少个2相乘后大于n,则会退出循环。由2x=n得到x= log2n。所以这个循环的时间复杂度为O( logn)。
-
平方阶
int i,j; for(i = 0; i < n; i++){ for(j = 0; j < n; j++){ /*时间复杂度为O(1)的程序步骤序列*/ } }
一个循环的时间复杂度为O(n),如果还嵌套一个循环就是循环的平方。所以这段代码的时间复杂度为O(n2)。
通过上面这些例子可以得出,其实理解大O推导并不算难,难的是对数列的一些相关运算,这更多的是考察你的数学知识能力和逻辑思维能力。因此一名合格的程序员,不能忽视数学知识。
-
常用时间复杂度及其耗费时间大小排序
执行次数函数 阶 非正式术语 12 O(1) 常数阶 2n+3 O(n) x 3n2+2n+1 O(n2) 平方阶 5log2n+20 I 对数阶 2n+3nlog2n+19 O(nlogn) nlogn阶 6n3+2n2+3n+4 O(n3) 立方阶 2n O(2n) 指数阶 O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
-
本文部分内容摘自《大话数据结构》清华大学出版社
函数的渐进增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们就说f(n)的增长渐进快于g(n) ↩︎