Skip to content

递归函数

20 / 44

递归(Recursion)

在函数中有个理解门槛比较高的概念递归函数(Recursive Functions)—— 那些在自身内部调用自身的函数说起来都比较拗口

先看一个例子我们想要有个能够计算 n 阶乘(factorial)n! 的函数f()规则如下

  • n! = n × (n-1) × (n-2)... × 1
  • n! = n × (n-1)!
  • n >= 1

注意以上是数学表达不是程序所以= 在这一小段中是等于的意思不是程序语言中的赋值符号

于是计算 f(n) Python 程序如下

python
def f(n):
    if n == 1:
        return 1
    else:
        return n * f(n-1)

print(f(5))
plaintext
120

递归函数的执行过程

factorial(5) 为例让我们看看程序的流程

f(5) 被调用之后函数开始运行……

  • 因为 5 > 1所以在计算 n * f(n-1) 的时候要再次调用自己 f(4)所以必须等待 f(4) 的值返回
  • 因为 4 > 1所以在计算 n * f(n-1) 的时候要再次调用自己 f(3)所以必须等待 f(3) 的值返回
  • 因为 3 > 1所以在计算 n * f(n-1) 的时候要再次调用自己 f(2)所以必须等待 f(2) 的值返回
  • 因为 2 > 1所以在计算 n * f(n-1) 的时候要再次调用自己 f(1)所以必须等待 f(1) 的值返回
  • 因为 1 == 1所以这时候不会再次调用 f() 于是递归结束开始返回这次返回的是 1
  • 下一步返回的是 2 * 1
  • 下一步返回的是 3 * 2
  • 下一步返回的是 4 * 6
  • 下一步返回的是 5 * 24 —— 至此外部调用 f(5) 的最终返回值是 120……

加上一些输出语句之后能更清楚地看到大概的执行流程

python
def f(n):
    print('\tn =', n)
    if n == 1:
        print('Returning...')
        print('\tn =', n, 'return:', 1)
        return 1
    else:
        r = n * f(n-1)
        print('\tn =', n, 'return:', r)
        return r

print('Call f(5)...')
print('Get out of f(n), and f(5) =', f(5))
plaintext
Call f(5)...
	n = 5
	n = 4
	n = 3
	n = 2
	n = 1
Returning...
	n = 1 return: 1
	n = 2 return: 2
	n = 3 return: 6
	n = 4 return: 24
	n = 5 return: 120
Get out of f(n), and f(5) = 120

有点烧脑…… 不过分为几个层面去逐个突破你会发现它真的很好玩

递归的终点

递归函数在内部必须有一个能够让自己停止调用自己的方式否则永远循环下去了……

其实我们所有人很小就见过递归应用只不过那时候不知道那就是递归而已听过那个无聊的故事罢

山上有座庙庙里有个和尚和尚讲故事……

山上有座庙庙里有个和尚和尚讲故事……

山上有座庙庙里有个和尚和尚讲故事……

写成 Python 程序大概是这样

python
def a_monk_telling_story():
    print('山上有座庙,庙里有个和尚,和尚讲故事,他说…… ')
    return a_monk_telling_story()

a_monk_telling_story()

这是个无限循环的递归因为这个函数里没有设置中止自我调用的条件无限循环还有个不好听的名字叫做死循环”。

在著名的电影盗梦空间2010从整体结构上来看,“入梦也是个递归函数”。只不过这个函数和 a_monk_telling_story() 不一样它并不是死循环 —— 因为它设定了中止自我调用的条件

在电影里醒过来的条件有两个

  • 一个是在梦里死掉
  • 一个是在梦里被 kicked ……

如果这两个条件一直不被满足那就进入 limbo 状态 —— 其实就跟死循环一样出不来了……

为了演示我把故事情节改变成这样

  • 入梦in_dream()是个递归函数
  • 入梦之后醒过来的条件有两个
  • 一个是在梦里死掉dead is True
  • 一个是在梦里被 kicked,kicked is True……

以上两个条件中任意一个被满足就苏醒……

至于为什么会死掉如何被 kick,我偷懒了一下管它怎样管它如何反正每个条件被满足的概率是 1/10……(也只有这样我才能写出一个简短的能够运行的盗梦空间程序”。)

把这个很抽象的故事写成 Python 程序看看一次入梦之后能睡多少天大概是这样

python
import random

def in_dream(day=0, dead=False, kicked=False):
    dead = not random.randrange(0,10) # 1/10 probability to be dead
    kicked = not random.randrange(0,10) # 1/10 probability to be kicked
    day += 1
    print('dead:', dead, 'kicked:', kicked)

    if dead:
        print((f"I slept {day} days, and was dead to wake up..."))
        return day
    elif kicked:
        print(f"I slept {day} days, and was kicked to wake up...")
        return day

    return in_dream(day)

print('The in_dream() function returns:', in_dream())
plaintext
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: True kicked: True
I slept 8 days, and was dead to wake up...
The in_dream() function returns: 8

如果疑惑为什么 random.randrange(0,10) 能表示 1/10 的概率请返回去重新阅读第一部分中关于布尔值的内容

另外 Python 若是需要将某个值与 True 或者 False 进行比较尤其是在条件语句中推荐写法是参见 PEP8):

python
if condition:
    pass

就好像上面代码中的 if dead: 一样

而不是虽然这么写通常也并不妨碍程序正常运行[1]):

python
if condition is True:
    pass

抑或

python
if condition == True:
    pass

让我们再返回来接着讲递归函数正常的递归函数一定有个退出条件否则的话无限循环下去了…… 下面的程序在执行一会儿之后就会告诉你RecursionError: maximum recursion depth exceeded上面那个山上庙里讲故事的和尚说的程序真要跑起来也是这样):

python
def x(n):
    return n * x(n-1)
x(5)















plaintext
RecursionError                            Traceback (most recent call last)

<ipython-input-3-daa4d33fb39b> in <module>
      1 def x(n):
      2     return n * x(n-1)
----> 3 x(5)

<ipython-input-3-daa4d33fb39b> in x(n)
      1 def x(n):
----> 2     return n * x(n-1)
      3 x(5)

... last 1 frames repeated, from the frame below ...

<ipython-input-3-daa4d33fb39b> in x(n)
      1 def x(n):
----> 2     return n * x(n-1)
      3 x(5)

RecursionError: maximum recursion depth exceeded

不用深究上面盗梦空间这个程序的其它细节不过通过以上三个递归程序 —— 两个很扯淡的例子一个正经例子 —— 你已经看到了递归函数的共同特征

  1. return 语句中返回的是自身的调用或者是含有自身的表达式
  2. 为了避免死循环一定要有至少一个条件下返回的不再是自身调用……

变量的作用域

再回来看计算阶乘的程序 —— 这是正经程序这次我们把程序名写完整factorial():

python
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

print(factorial(5))
plaintext
120

最初的时候这个函数的执行流程之所以令人迷惑是因为初学者对变量作用域把握得不够充分

变量根据作用域可以分为两种全局变量(Global Variables)和局部变量(Local Variables)。

可以这样简化理解

  • 在函数内部被赋值而后使用的都是局部变量它们的作用域是局部无法被函数外的代码调用
  • 在所有函数之外被赋值而后开始使用的全局变量它们的作用域是全局在函数内外都可以被调用

定义如此但通常程序员们会严格地遵守一条原则

在函数内部绝对不调用全局变量即便是必须改变全局变量也只能通过函数的返回值在函数外改变全局变量

你也必须遵守同样的原则而这个原则同样可以在日常的工作生活中调用”:

做事的原则自己的事自己做别人的事最多通过自己的产出让他们自己去搞……

再仔细观察一下以下代码当一个变量被当做参数传递给一个函数的时候这个变量本身并不会被函数所改变比如a = 5而后再把 a 当作参数传递给 f(a) 的时候这个函数当然应该返回它内部任务完成之后应该传递回来的值 a 本身不会被改变

python
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

a = 5
b = factorial(a)   # a 并不会因此改变;
print(a, b)
a = factorial(a)   # 这是你主动为 a 再一次赋值……
print(a, b)
plaintext
5 120
120 120

理解了这一点之后再看 factorial() 这个递归函数的递归执行过程你就能明白这个事实

在每一次 factorial(n) 被调用的时候它都会形成一个作用域n 这个变量作为参数把它的值传递给了函数但是n 这个变量本身并不会被改变

我们再修改一下上面的代码

python
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

n = 5              # 这一次,这个变量名称是 n
m = factorial(n)   # n 并不会因此改变;
print(n, m)
plaintext
5 120

m = factorial(n) 这一句中n factorial() 当做参数调用了但无论函数内部如何操作并不会改变变量 n 的值

关键的地方在这里在函数内部出现的变量 n和函数外部的变量 n 不是一回事 —— 它们只是名称恰好相同而已函数参数定义的时候用别的名称也没什么区别

python
def factorial(x): # 在这个语句块中出现的变量,都是局部变量
    if x == 1:
        return 1
    else:
        return x * factorial(x-1)

n = 5           # 这一次,这个变量名称是 n
m = factorial(n)   # n 并不会因此改变;
print(n, m)
# 这个例子和之前再之前的示例代码有什么区别吗?
# 本质上没区别,就是变量名称换了而已……
plaintext
5 120

函数开始执行的时候x 的值是由外部代码函数被调用的那一句传递进来的即便函数内部的变量名称与外部的变量名称相同它们也不是同一个变量

递归函数三原则

现在可以小小总结一下了

一个递归函数之所以是一个有用有效的递归函数是因为它要遵守递归三原则正如一个机器人之所以是个合格的机器人是因为它遵循阿西莫夫三铁律(Three Laws of Robotics)一样[2]

  1. 根据定义递归函数必须在内部调用自己
  2. 必须设定一个退出条件
  3. 递归过程中必须能够逐步达到退出条件……

从这个三原则望过去factorial() 是个合格有效的递归函数满足第一条满足第二条尤其还满足第三条中的逐步达到”!

而那个扯淡的盗梦空间递归程序说实话不太合格虽然它满足第一条也满足第二条第三条差点蒙混过关它不是逐步达到而是不管怎样肯定能达到 —— 这明显是两回事…… 原谅它罢它的作用就是当例子一次正面的一次负面的作为例子算是功成圆满了

刚开始的时候初学者好不容易搞明白递归函数究竟是怎么回事之后就不由自主地想我如何才能学会递归式思考呢?” —— 其实吧这种想法本身可能并不是太正确或者准确

准确地讲递归是一种解决问题的方式当我们需要解决的问题可以被逐步拆分成很多越来越小的模块然后每个小模块还都能用同一种算法处理的时候用递归函数最简洁有效所以只不过是在遇到可以用递归函数解决问题的时候才需要去写递归函数

从这个意义上来看递归函数是程序员为了自己方便而使用的并不是为了计算机方便而使用 —— 计算机么你给它的任务多一点或者少一点对它来讲无所谓反正有电就能运转它自己又不付电费……

理论上来讲所有用递归函数能完成的任务不用递归函数也能完成只不过代码多一点啰嗦一点看起来没有那么优美而已

还有递归不像序列类型那样是某个编程语言的特有属性它其实是一种特殊算法也是一种编程技巧任何编程语言都可以使用递归算法都可以通过编写递归函数巧妙地解决问题

但是学习递归函数本身就很烧脑啊这才是最大的好事从迷惑到不太迷惑到清楚到很清楚再到特别清楚 —— 这是个非常有趣非常有成就感的过程

这种过程锻炼的是脑力 —— 在此之后再遇到大多数人难以理解的东西你就可以使用这一次积累的经验应用你已经磨炼过的脑力有意思

至此封面上的那个伪代码应该很好理解了

python
def teach_yourself(anything):
    while not create():
        learn()
        practice()
    return teach_yourself(another)

teach_yourself(coding)

自学还真的就是递归函数呢……

思考与练习

普林斯顿大学的一个网页有很多递归的例子

https://introcs.cs.princeton.edu/java/23recursion/


脚注

[1]参见 Stackoverflow 上的讨论Boolean identity == True vs is True

↑Back to Content↑

[2]关于阿西莫夫三铁律(Three Laws of Robotics)的类比来自著名的 Python 教程Think Python: How to Think Like a Computer Scientist

↑Back to Content↑