时间复杂度
实现算法程序的执行时间可以反应出算法的效率,即算法的优劣。
一、“好”算法的标准
对于一个问题的算法来说,之所以称之为算法,首先它必须能够解决这个问题(称为准确性
)。其次,通过这个算法编写的程序要求在任何情况下不能崩溃(称为健壮性
)。
如果准确性和健壮性都满足,接下来,就要考虑最重要的一点:通过算法编写的程序,运行的效率怎么样。
运行效率体现在两方面:
- 算法的运行时间。(称为“时间复杂度”)
- 运行算法所需的内存空间大小。(称为“空间复杂度”)
好算法的标准就是:在符合算法本身的要求的基础上,使用算法编写的程序运行的时间短,运行过程中占用的内存空间少,就可以称这个算法是“好算法”。
调查表明,人们对于软件或者 APP 的运行效率有极高的要求,例如对于网页打开的忍耐极限是 6 秒甚至更短,如果你设计的网页打开的时间超过 6 秒,多数人会在 4 秒甚至 3 秒的时候毫不犹豫地关掉而去浏览其他网页。在这个大背景下,一个好的“算法”更注重的是时间复杂度,而空间复杂度只要在一个合理的范围内就可以。
二、时间复杂度的计算
计算一个算法的时间复杂度,不可能把所有的算法都编写出实际的程序出来让计算机跑,这样会做很多无用功,效率太低。实际采用的方法是估算算法的时间复杂度。
之前讲过,程序由三种结构构成:顺序结构、分支结构和循环结构。顺序结构和分支结构中的每段代码只运行一次;循环结构中的代码的运行时间要看循环的次数。
由于是估算算法的时间复杂度,相比而言,循环结构对算法的执行时间影响更大。
所以,
算法的时间复杂度,主要看算法中使用到的循环结构中代码循环的次数(称为“频度”)。次数越少,算法的时间复杂度越低。
例如:(C语言代码)
a) ++x; s=0;
b) for (int i=1; i<=n; i++) { ++x; s+=x; }
c) for (int i=1; i<=n; i++) { for (int j=1; i<=n; j++) { ++x; s+=x; } }
上边这个例子中,a 代码的运行了 1
次,b 代码的运行了 n
次,c 代码运行了 n*n
次。
三、时间复杂度的表示
算法的时间复杂度的表示方式为:
O(频度)
这种表示方式称为大“O”记法
。
注意,是大写的字母
O
,不是数字0
。
对于上边的例子而言,a 的时间复杂度为O(1)
,b 的时间复杂度为O(n)
,c 的时间复杂度为为O(n^2)
。
如果a、b、c组成一段程序,那么算法的时间复杂度为O(n^2+n+1)
。但这么表示是不对的,还需要对n^2+n+1
进行简化。
简化的过程总结为3步:
- 去掉运行时间中的所有加法常数。(例如
n^2+n+1
,直接变为n^2+n
) - 只保留最高项。(
n^2+n
变成n^2
) - 如果最高项存在但是系数不是
1
,去掉系数。(n^2
系数为1
)
所以,最终a、b和c合并而成的代码的时间复杂度为O(n^2)
。
四、常用的时间复杂度的排序
列举了几种常见的算法时间复杂度的比较(又小到大):
O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n^2)平方阶 < O(n^3)(立方阶) < O(2^n) (指数阶)
五、拿时间换空间,用空间换时间
算法的时间复杂度和空间复杂度是可以相互转化的。
谷歌浏览器相比于其他的浏览器,运行速度要快。是因为它占用了更多的内存空间,以空间换取了时间。
算法中,例如判断某个年份是否为闰年时,如果想以时间换取空间,算法思路就是:当给定一个年份时,判断该年份是否能被4或者400整除,如果可以,就是闰年。
如果想以空间换时间的话,判断闰年的思路就是:把所有的年份先判断出来,存储在数组中(年份和数组下标对应),如果是闰年,数组值是1,否则是0;当需要判断某年是否为闰年时,直接看对应的数组值是1还是0,不用计算就可以马上知道。
六、最坏时间复杂度
分析算法时,存在几种可能的考虑:
- 算法完成工作最少需要多少基本操作,即最优时间复杂度
- 算法完成工作最多需要多少基本操作,即最坏时间复杂度
- 算法完成工作平均需要多少基本操作,即平均时间复杂度
比如:如果要排序一个数组,然后这个数组是已经排序好的数组,那么对这个数组进行排序,速度会非常快。这属于最好情况。
我们主要关注算法的最坏情况,亦即最坏时间复杂度。
常用时间复杂度示例
def hello():
print('hello')
这个函数执行了 1 代码。 渐进复杂度:O(1)
def hello1():
print('hello')
print('hello')
print('hello')
这个函数执行的代码次数是 3。复杂度是: O(1)
def hello2():
for _ in range(20):
print('hello')
这段代码我们就说,这段代码执行了20次语句。
def hello3():
for _ in range(20):
print('hello')
print('hello')
print('hello')
这段代码花费的时间是:60
。复杂度是: O(1)
def hello4(n):
for _ in range(n):
print('hello')
print('hello')
print('hello')
这段代码花费的时间是:3n
。复杂度是: O(n)
def hello5(n):
for _ in range(n):
print('hello') # 被执行 n 次
print('hello') # 被执行 n 次
for _ in range(n):
print('hello') # 被执行 n * n 次(n的平方)
print('hello') # 被执行 n * n 次(n的平方)
这段代码花费的时间是:n + n + n^2 + n^2 = 2n + 2n^2 = 2(n^2+n)
。复杂度是:O(n^2)
总结
代码花费的时间主要就是通过代码被执行的次数来判断,有一些复杂的代码是需要很强的数学功底才能算出执行的次数,对于此我们知道常见的即可。