例子1: 斐波那契数列的定义如下:F1 = 1, F2 = 1, Fn =Fn–1 + Fn–2 (n ≥ 3)。如果用递归算法计算斐波那契数列的第 n 项,则其时间复杂度为O(Fn)
O(Fn)为指数形式。具体可以从Fn的通项公式中看出:
递归的代码如下:
def fibonacci(n):
if n == 1:
return 0
if n == 2:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
这里值得说明的是这种递归方式之所以复杂度达到了指数级别,是因为算法实现过程存在重复计算过程,以f(6)为例,递归树结构如下:
可以看到递归计算第6项时,需要重复计算两次f(4),三次f(3),以二叉树的结构向下延伸,如测试的例子所示,当计算到第40项时,会有36项都需要重复计算,且越往下,重复次数越多,效率也很低下。
那么算法怎么改进呢?
思路一:通过尾递归实现
def lastFibonacci(n, ret1, ret2):
if(n == 1):
return ret1
return lastFibonacci(n - 1, ret2, ret1 + ret2)
尾递归快就快在它不需要重复计算某一项,利用了一种技巧就是每次递归调用时,它会把之前已经计算好的结果以参数的形式传递过去,同样以f(6)为例:
n | ret1 | ret2 |
---|---|---|
6 | 0 | 1 |
5 | 1 | 1 |
4 | 1 | 2 |
3 | 2 | 3 |
2 | 3 | 5 |
1 | 5 | 8 |
思路二:动态规划的思想:迭代实现
def fibonacci(n):
a = 0
b = 1
result = 0;
if n == 1:
return 0
if n == 2:
return 1
for i in range(3, n+1):
result = a +b
a = b
b = result
return result
补充说明:尾递归是什么?
还是举例子说明吧
以求阶乘为例:
非尾递归版本
def fact(n):
if n < 0:
return 0
elif n == 0 || n == 1:
return 1
else
return n * fact(n - 1)
尾递归版本
def facttail(n, res):
if n < 0:
return 0
elif n == 0:
return 1
elif n == 1:
return res
else:
return facttail(n - 1, n*res)
代码1:在每次函数调用计算n倍的(n-1)!的值,让n=n-1并持续这个过程直到n=1为止。这种定义不是尾递归的,因为每次函数调用的返回值都依赖于用n乘以下一次函数调用的返回值,因此每次调用产生的栈帧将不得不保存在栈上直到下一个子调用的返回值确定。
代码2:函数比代码1多个参数res,除此之外并没有太大区别。res(初始化为1)维护递归层次的深度。这就让我们避免了每次还需要将返回值再乘以n。然而,在每次递归调用中,令res=n*res并且n=n-1。继续递归调用,直到n=1,这满足结束条件,此时直接返回res即可。
可以仔细看看两个函数的具体实现,看看递归和尾递归的不同!
示例中的函数是尾递归的,因为对facttail的单次递归调用是函数返回前最后执行的一条语句。换句话说,在递归调用之后还可以有其他的语句执行,只是它们只能在递归调用没有执行时才可以执行。
尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如sum(n) = f(n) = f(n-1) + value(n) ;会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。
例子2: 设某算法的计算时间表示为递推关系式T(n)= T(n -1) + n(n 为正整数)及T(0) = 1,则该算法的时间复杂度为O(n^2)
T(n)=T(n-1)+n (1)
T(n-1)=T(n-2)+n-1 (2)
T(n-2)=T(n-3)+n-2 (3)
T(n-3)=T(n-4)+n-3 (4)
……
T(3)=T(2)+3 (n-2)
T(2)=T(1)+2 (n-1)
T(1)=T(0)+1 (n)
将(n)式带回(n-1) 式,将(n-1)式带回(n-2) 式,将式子依次带回,最后带回(4) 式,(3) 式,(2) 式,(1) 式。带入式子结果如下:
T(n)=T(0)+1+2+3+……+n-3+n-2+n-1+n
计算结果如下:
T(n)=1+1+2+3+……+n-3+n-2+n-1+n
T(n)=1+(1+n)*n/2
例子3:假设某算法的计算时间可用递推关系式T(n)=2T(n/2)+n表示,则该算法的时间复杂度为O(nlog2(n))
T(n)=2T(n/2),所以T(n)/n=T(n/2)/(n/2)+1,
T(n/2)/(n/2)=T(n/4)/(n/4)+1;
T(n/4)/(n/4)=T(n/8)/(n/8)+1;
...
T(2)/2=T(1)/1+1;
这些等式左右相加,T(n)/n=T(1)+1*log2(n);
即T(n)=nT(1)+n*long2(n);
复杂度为O(nlog2(n))