我们知道计算机适合做一些重复性的工作,且计算机求解问题遵循的一个基本原则是将问题规模逐渐减小,即将原问题转化为与之相似的规模较小的子问题。递归算法就是通过调用自身将问题规模缩小的算法设计思想。
递归的定义
递归就是在函数内部直接或间接调用自身。
def fib(n):
if n <= 1:
return 1
return fib(n - 1) + fib(n - 2)
如上求解Fibonacci数列的函数,在函数fib内部调用了自身fib。
递归三要素
编写一个递归函数一般要满足以下三个条件:
- 要有Base:即递归要有终结的标准,如上fib函数的base就是n<=1,当n逐渐减小到1时,问题变得容易,递归终结,直接返回结果。
- 缩小规模:递归最主要的作用之一就是将问题规模缩小,如上fib函数中n-1和n-2就是逐步减小n。
- 调用自身:调用自身就是在重复处理问题的逻辑过程,以解决子问题。
递归函数执行过程
一个递归函数的完整执行可分为两个过程:
- 调用:上层函数调用下层函数(规模小一级的函数)
- 回退:下层函数将计算结果返回给上层函数
从上图可见,递归的效率其实很低,因为在调用过程中,系统要开辟栈空间存储每一层函数的返回结果以及局部变量,递归过深容易造成栈溢出。另外,对于fib函数而言,重复计算了多个n,用递推的方式保存中间计算过程可解决该问题。
如何更好地理解递归?
理解递归关键在于抓住子问题的结构以及迭代式(上层问题与下层子问题的交接过程),不要总是企图去理解并跟踪整个递归过程。
有个技巧,可以将递归函数内调用的自身函数理解为一个实现相同功能的同名函数,其执行的结果直接返回给了顶层函数,那么最顶层函数执行了一个可获得最终结果的完整逻辑过程。
下文将以汉诺塔为例来讲解如何去理解递归。
举个例子-汉诺塔(Hanoi)
以汉诺塔为例进一步加深对递归的理解。
简单介绍Hanoi
假设有n个圆盘,最初从下到上按大小叠加在A柱上,每次移动一个圆盘,最终将所有圆盘移动到C柱上,且过程中小盘必须位于大盘之上。
过程剖析
第一步:
第二步:
第三步:
- 显然,当n=1时,可直接将盘从A柱移向C柱。(Base)
- n>1时,先移动最底下的盘到C柱,在以同样的逻辑处理上面的n-1个盘。(缩小规模)
- n个盘为上层,n-1个盘为下层,按照上图过程,将n-1个盘先移动到B柱,再移动到C柱,即完成了与上层的衔接。n-1个盘的处理过程调用自身即可。(调用自身返回子问题结果,完成上下层交接)
代码分析
def hanoi(n, A, B, C):
# A参数为起始柱,B参数为中间柱,C参数为目的柱
assert n > 0, 'n>0, 至少一个盘'
if n == 1:
print('{0}-->{1}'.format(A, C))
else:
hanoi(n - 1, A, C, B) # 该函数实现将n-1个盘从起始柱A柱移动到中间柱B柱的功能
print('{0}-->{1}'.format(A, C))
hanoi(n - 1, B, A, C) # 该函数实现将n-1个盘从中间柱B柱移动到目的柱C柱的功能
if __name__ == '__main__':
hanoi(3, 'A', 'B', 'C')
"""
游戏执行步骤:
A-->C
A-->B
C-->B
A-->C
B-->A
B-->C
A-->C
"""
参考资料
https://www.zhihu.com/question/24385418
图片来源