fib()相关的一些事

所有代码均来自于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的组合实现了递归写法,在某些问题上降低了编程时间的消耗,在某些场景下可以考虑作为一种新的思路。

    原文作者:MontyOak
    原文地址: https://www.jianshu.com/p/9097691d8409
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞