所有代码均来自于Python 2.7 版本
相信对于所有有过编程经历的童鞋而言,递归都是一个再熟悉不过的概念。而在初学递归的时候,相信斐波那契数列都是一个重要的例子(另一个则是汉诺塔(Hanoi))。今天就利用求第n项斐波那契数列作为一个例子,来简单说一下我对几个概念的理解。
递归
话不多说,直接上代码就好
def fib(n):
if n<2:
return 1
else:
return fib(n-1) + fib(n-2)
//再简单点
fib = lambda x:x if x<2 else fib(x-1)+fib(x-2)
稍有一点递归概念的童鞋都不难理解这几行程序,这也是递归的优势:简单,易编写。但同时,它也有一个非常致命的缺点,就是运行效率低(具体原因是存在大量的重复运算,具体过程可以手动算一个fib(10)体会一下,看看fib(1)到底调用了多少次)。
更为致命的是,Python还对递归栈的深度做了限制,也就是说稍微大一点的数都会抛出异常。
//对于上面已经定义好的fib函数进行调用
>>> fib(1000)
...
RuntimeError: maximum recursion depth exceeded
显然,1000已经超出了Python的最大递归深度。那么,有没有什么方法对递归进行优化呢?
迭代
一般认为,所有的递归都可以写成迭代,区别在于编程时间的花费以及代码量、可读性方面的问题。
简单思考一下,写出下面代码应该不难。
def fib(n):
a, b = 1, 1
if n<3:
return b
for _ in range(3, n):
a, b = b, a+b
return b
很显然,和递归的思考方式相反,递归是从顶向下,减而治之;迭代是从底向上,依次计算。
尾递归
尾递归是递归的一种,区别在于内存的占用(即递归栈深度的限制):尾递归只占用恒量的内存;而普通递归则是创建递归栈之后计算,随着输入数据的增大,引发递归深度增大,因而内存占用增大。详细解释
接下来就对上面的递归版本的fib()
进行一个优化。
def fib(count, tmp=1, next_step=1):
if count < 2:
return tmp
else:
return fib(count-1, next_step, next_step+tmp)
其中,最后一行代码才是尾递归优化的部分。可以看出,相比普通的递归函数,尾递归每次只返回一个fib()
并且通过count
递减来保证减而知之和避免重复计算。
下面,就让我们来试一下这个版本的fib()
。
//对于上面已经定义的尾递归版本fib函数进行调用
>>> fib(1000)
...
RuntimeError: maximum recursion depth exceeded
意外发现,竟然和普通递归抛出一样的异常。这是什么原因呢?简单搜索一下,悲剧的得知,Python
在语言层面,并不支持尾递归优化且对递归次数有限制。
Yield
那么,就没有办法对于大规模输入使用递归写法了吗?
答案是有的。想想看,递归之所以无法工作,根本原因在于内存的大量占用和递归深度超限。所以说,只要削减递归占用的内存,就可以使用递归写法,享受递归带来的方便了。
yield
关键字恰好是具有这种功能的解决方案。它产生一个生成器,它具有惰性求值的属性。对内存的占用是常数级别的,不随输入规模增大而增大。
def fib(count, tmp=1, next_step=1):
if count < 2:
yield tmp
else:
yield fib(count-1, next_step, next_step+tmp)
调用的时候
b = fib(1000)
for _ in xrange(1000):
b = b.next()
print b
效率
最后,利用运行时间,来比较一下迭代算法和尾递归的效率。
首先,写一个计算运行时间的装饰器。
def timeit(fun):
def wrapped(*args, **kwargs):
import time
t1 = time.time()
execFun = fun(*args, **kwargs)
//为了体现差别,让函数执行1000次
for _ in xrange(1000):
fun(*args, **kwargs)
t2 = time.time()
print t2 - t1
return execFun
return wrapped
def fib1_helper(count, tmp=1, next_step=1):
if count < 2:
yield tmp
else:
yield fib1_helper(count-1, next_step, next_step+tmp)
@timeit
def fib1(n):
b = fib1_helper(n)
for _ in xrange(n):
b = b.next()
@timeit
def fib2(n):
a, b = 1, 1
if n<3:
return b
for _ in range(3, n):
a, b = b, a+b
return b
//分别调用
>>> fib1(1000)
>>> fib2(1000)
//以下执行结果因执行环境不同可能存在差异
0.641000032425
0.0920000076294
可以看出,相比递归,迭代在执行时间上还是有着不小的优势。但是尾递归+yield的组合实现了递归写法,在某些问题上降低了编程时间的消耗,在某些场景下可以考虑作为一种新的思路。