|
|
|
|
移动端

3.1.6 函数的递归

《自学Python:编程基础、科学计算及数据分析》第3章Python 进阶,在本章中,我们将学习 Python的一些进阶用法,包括函数的进阶,迭代器、生成器、装饰器、上下文管理器的使用,以及 Python中的变量作用域。本节为大家介绍函数的递归。

作者:李金来源:机械工业出版社|2018-05-04 13:27

人工智能+区块链的发展趋势及应用调研报告


3.1.6 函数的递归

递归(Recursion)是指函数在执行的过程中调用了本身,通常用于分治法( DivideAnd Conquer),即将问题拆分为规模较小的子问题。

例如,阶乘函数可以写成:

  1. f (n) = n! = n×(n-1)! = n×f (n-1) 

我们把求解 n阶乘的问题变成了一个求解 n-1阶乘的问题,以此类推,我们只需要解决最简单的f(1)的问题。其函数定义如下:

  1. In [1]: def fac(n):  
  2. ...: return 1 if n == 1 else n * fac(n-1)  
  3. ...:  
  4. In [2]: fac(6)  
  5. Out[2]: 720 

利用递归,我们将 fac()函数写成了一种非常紧凑的形式,如果 n为 1,返回 1,否则

返n*fac(n-1)。递归可以更快地实现代码,不过在效率上可能会有一定的损失。

例如,斐波那契数列是这样的一个数列:1、1、2、3、5、8、13……其规律为:

  1. F(0) = F(1) = 1, F(n) = F(n-1) + F(n-2) 

即后一个数是前面两个数的和。

按照这个逻辑,我们很容易就能写出一个递归版本的代码:

  1. In [3]: def fib1(n):  
  2. ...: return 1 if n <= 1 else fib1(n-1) + fib1(n-2)  
  3. ...:  
  4. In [4]: map(fib1, range(10))  
  5. Out[4]: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] 

用非递归的方式的实现:

  1. In [5]: def fib2(n):  
  2. ...: a, b = 1, 1  
  3. ...: for _ in range(n):  
  4. ...: a, bb = b, a+b  
  5. ...: return a  
  6. ...:  
  7. In [6]: map(fib2, range(10))  
  8. Out[6]: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] 

非递归的版本基本过程如下:

初始情况:a=F(0),b=F(1)

第一轮更新:a=F(1),b=F(0)+F(1)=F(2)

第二轮更新:a=F(2),b=F(3) ……

第n轮更新:a=F(n),b=F(n+1) 利用IPython的魔术命令%timeit,我们对这两个函数的运行时间进行比较:

  1. In [7]: %timeit fib1(20)  
  2. 100 loops, best of 3: 5.35 ms per loop  
  3. In [8]: %timeit fib2(20)  
  4. 100000 loops, best of 3: 2.2 μs per loop 

可以看到,两者的效率有很大的差别,非递归版本比递归版本要快很多。递归版本中存在大量的重复计算,例如,当我们调用 fib1(n)时,需要计算一次 fib1(n-1)和一次 fib1(n-2),而调用fib1(n-1)时需要再调用一次fib1(n-2),这样fib1(n-2)事实上被计算了两次。

为了减少重复计算,可以考虑使用缓存机制来实现一个更快的递归版本,它利用默认参数可变的性质,使用缓存保存已经计算的结果:

  1. In [9]: def fib3(n, cache={0:1, 1:1}):  
  2. ...: try:  
  3. ...: return cache[n]  
  4. ...: except KeyError:  
  5. ...: cache[n] = fib3(n-1) + fib3(n-2)  
  6. ...: return cache[n]  
  7. ...:  
  8. In [10]: map(fib3, range(10))  
  9. Out[10]: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] 

默认参数 cache初始化为一个字典,并且存储初始值 F(0)、F(1),它起到一个缓存的作用。计算 fib(n)时,首先在缓存 cache中查找,如果 cache中有键n,直接返回结果;如果没有,抛出一个KeyError,在except的部分,函数使用递归更新cache[n]的值,然后返回它。

对于该函数,调用 fib3(n-1)使得缓存中保存了 cache[n-2],再调用 fib3(n-2)的时候会直接返回结果而不是重复计算,提高了计算效率。

带缓存的递归版本的时间效率:

  1. In [11]: %timeit fib2(20)  
  2. 1000000 loops, best of 3: 230 ns per loop 


喜欢的朋友可以添加我们的微信账号:

51CTO读书频道二维码


51CTO读书频道活动讨论群:365934973

【责任编辑:book TEL:(010)68476606】

回书目   上一节   下一节
点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

程序员密码学

《程序员密码学》涉及密码学的各个研究方向,分组密码、散列函数、公钥密码以及相关的攻击,同时也讲解了密码学算法实现上常用的ASN.编码、...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊