衡量一个算法的复杂度主要从时间维度和空间维度两个方面,时间复杂度是指执行算法所消耗的总时间,空间复杂度是指执行算法所占用的内存空间。
1.时间复杂度
代码执行的时间只有在跑完后才能知道的,所以在设计算法之前,只能预先估计算法的时间复杂度。
预估时间复杂度使用代码执行的总次数。假设cpu每个单元执行消耗的时间一样,也就是假设每行代码执行时间一样,用f(n)表示代码执行的总次数,比如下面代码中,f(n) = n+n。
for i in range(n): # n次
a += 1 # n次
我们一般考虑最坏的情况,即n趋于无穷的时候,用O(f(n))描述时间复杂度,O可以理解为上界的意思,此时O(f(n)) = O(n),因为n趋于无穷的时候2n和n已经没有区别了,这就是经常遇见的复杂度。
时间复杂度 O() 描述的是一个量级,是一个预估量,常见的有:O(1)、 O(logn)、 O(n)、 O(nlogm)、 O( n 2 n^2 n2)、 O( n 3 n^3 n3)
它们的优劣顺序为(越小越好):
O(1) < O(logn) < O(n) < O(nlogm) < O( n 2 n^2 n2) < O( n 3 n^3 n3)
1)O(1)
一般不涉及循环的,时间复杂度都是常数阶,因为每行代码都只执行一次。
a = list()
b = [1,2,3]
a.append(b)
或者:
a = 1
b = 1
if a == b:
print('a == b')
else:
print('a != b')
等都是O(1)
2)O(n)
这就是上面举的例子,一般涉及一个循环,下面是一个复杂点的例子:
a = 1 #1次
b = 2 #1次
for i in range(n): #n次
a += 1 #n次
c = a+b #n次
print c #n次
f(n) = 1+1+n+n+n+n = 4n+2,同样的道理,在最坏的情况下,n趋于无穷的时候,(4n+2)和n是一样的,时间复杂度为O(n),也就是说,我们在计算时间复杂度的时候,是忽略常数项和常数系数的,因为常数和无穷大的 n 不是一个量级,所以,在计算时间复杂度时,直接看循环、迭代、递归的部分。
下面的while循环也是一样:
p = 0
while p < n:
p += 1
3)O(log n)
x = 1
while x < n:
x = x*2
上面代码假设循环 a 次后x>=n,那么( 2 a 2^a 2a) * x = n,求得a = log n,所以时间复杂度为 O(log n) 。当然,如果x不是1,或者循环里面不是2倍关系而是3倍甚至10倍呢?比如下面代码:
a = [1,2,3,4,5,6,7,8]
x = len(a)
while x < n:
x = 3*x
这样求得循环次数 a = ( log 3 n \log_3n log3n)/4 ,在这里还是忽略常数系数1/4,底数3也忽略,都记作 O(log n),因为对量纲的影响不大, log ? n \log_? n log?n 始终是小于 n 的,不管底数是什么。实际上底数越来越大值反而越来越小,对应的复杂度也就越小,所以我们都默认为最坏的情况,系数为1、底数为2,底数为2时可以不写,记为O(log n)。
4)O(nlog m)
这个很容易理解,见代码:
for i in range(n): #n次
while x<m: #logm 次
x = 3*x
因为for循环,每次循环里面又套了logm次while循环,所以为O(nlogm)
5)O( n 2 n^2 n2)
#input: a:list()
b = 0
for i in range(n): #n次
for j in range(n): #n次
if a[i] == a[j]: #a是输入列表
b += 1
与上面同理,只不过第二个循环的次数与第一个一样,也可以不一样:
a = 0
for i in range(n): # n
for j in range(m): # m
a += 1
不过当n和m都趋于无穷大时,就可以认为n == m,复杂度为O( n 2 n^2 n2).
for、while循环 理论上是可以一直套的,套三个就是O( n 3 n^3 n3),套四个就是O( n 4 n^4 n4),不过一般时间复杂度超过O( n 3 n^3 n3),算法执行所消耗的时间就有点多了,所以一般控制在O( n 3 n^3 n3)之内,能不嵌套就不嵌套。
在实际工程当中,数据集比较大的情况下,比如n可能是数据的长度,可以使用GPU进行加速计算,有兴趣的话后面会写一篇关于python使用GPU加速计算的文章。
2.空间复杂度
一般python中的空间复杂度主要取决于变量的长度,表示临时占用的内存空间,比如:
a = 1 # O(1)
a = [1,2,3,....,n] #O(n)
a = [[1,3,...,n],[2,4,...,n]] #O(n^2)
a = [[1,2,...,n],[1,2,...,n],...,[1,2,...,n]] #O(n^n)
另外,有些算法本身空间复杂度会比较大,比如递归算法:
#a is input:list();len(a) == n
def sum(a):
if len(a) <= 1:
return a[0]
else:
return a[0]+sum(a[1:])
如果 a 的长度为 n ,那么在计算过程中会申请n个临时的内存空间,空间复杂度为O(n),另外递归算法的时间复杂度也是O(n),因为它执行了n次。
在实际写算法时,只要代码不是过于冗长,一般对空间复杂度不会太过于看重,主要是时间复杂度。
另外,以上描述的复杂度都是对n趋于无穷时算法复杂度的估计,准确说是对一种趋势的估计**,一种对最坏的趋势的估计。**在实际写代码的时候,比如如果是在刷Leetcode,输入是非常大的测试集且未知,就要以最坏的情况考虑代码;如果输入是确定的,即n的值是可观的,就可以具体估计复杂度。