这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
上篇文章在比较算法优劣的时候对同一组输入的执行时间来判断了一个算法的好坏。这样的话还是需要写代码去测试。这种方法需要在测试代码执行完成之后才能判断,这样也太麻烦了,所以就出现了大O表示法用来估算算法的好坏。
递归方法计算斐波那契数列
从上篇的文章斐波那契数列第N项值的计算,现在来看看两个算法使用大O表示法怎么表示。最后再来研究该到底怎么使用大O表示法来体现算法的复杂度。
def fib(n):
if n<=1:
return n
else:
return fib(n-1)+fib(n-2)
递归方法计算斐波那契数列第N项的值的时候函数会被重复调用多次,我们来举个例子
n=3的时候,会计算fib(2)+fib(1),其中fib(2)函数会计算fib(1)+fib(0),fib(1)和fib(0)会直接返回。
所以n=3的时候,fib函数会执行5次:
graph TD
A["fib(3)"]-->B["fib(2)"]
A-->C["fib(1)"]
B-->D["fib(1)"]
B-->E["fib(0)"]
n与函数调用次数的关系可以表示T(3) = 5
n=4的时候,会计算fib(3)+fib(2),其中fib(3)会计算fib(2)+fib(1),其中fib(2)会计算fib(1)+fib(0),整个函数调用中会有两个fib(2)会调用,每个fib(2)会调用fib(1)+fib(0)。
所以n=4时,整个函数调用过程会执行如下图:
graph TD
A["fib(4)"]-->B["fib3)"]
B-->F["fib(2)"]
B-->G["fib(1)"]
F-->H["fib(1)"]
F-->I["fib(0)"]
A-->C["fib(2)"]
C-->D["fib(1)"]
C-->E["fib(0)"]
一共会调用1+2+4+2=9,9次fib()函数。N与函数调用次数的关系可以表示T(4) = 9
再画个图表示N等于5时调用次数数一数有多少个:
graph TD
A["fib(5)"]-->B["fib(4)"]
A-->C["fib(3)"]
B-->D["fib(3)"]
B-->E["fib(2)"]
D-->F["fib(2)"]
D-->G["fib(1)"]
F-->H["fib(1)"]
F-->I["fib(0)"]
E-->J["fib(1)"]
E-->K["fib(0)"]
C-->L["fib(2)"]
C-->M["fib(1)"]
L-->N["fib(1)"]
L-->O["fib(0)"]
仔细数数一共调用了1+2+4+6+2=15次fib()函数
21+22+23 = 24-1
那么怎么用通用的公式表示呢:
3->3
4->9
5->15
an = 2(N-1)-1
用大O表示法为 :O(2N)
为什么要这么表示我们放在后面说。
普通循环的时间复杂度
接下来我们看下普通的循环方法调用了几次函数。
def fib(n):
if n<=1:
return n
n1 = 0
n2 = 1
for i in range(n-1):
res = n1+n2
n1 = n2 #随着循环不断更新n1和n2的值
n2 = res
return res
n与函数调用次数的关系可以表示an = n
用大O表示法为 :O(N)
大O表示法
因为大O表示法是一种粗略的估算的分析方法,所以会忽略一些常数,系数,低阶的部分。
例如在递归方法an = 2(N-1)-1 中1就是常数,相对于幂函数的部分也是低阶的部分。
大O表示法虽然是一种估算方法,但是它指出了最糟糕的情况,因为在输入参数尽可能大的时候那些常数,低阶和系数的部分几乎可以忽略不计。
总结
常见的时间复杂度表示法,由快到慢:
-
O(1)
-
O(log n)
-
O(n)
-
O(n * log n)
-
O(2N)
-
O(n!)
对应直观的变化曲线:
都是随着输入的参数变大,耗费的时间越长,可以看出这五种变化曲线中除了O(1),O(log n) 变化曲线是最慢的。