master
/ 4.5 递归.ipynb

4.5 递归.ipynb @masterview markup · raw · history · blame

Notebook

4.6 递归

1.斐波那契数列前n项

0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,…… 斐波那契数列又称黄金分割数列,这个数列从第3项开始,后一项的值总是与他前面两项的和的值相等。

在数学上,以递推的方法定义:

F(1)=0
F(2)=1 
F(n)=F(n - 1)+F(n - 2)    (n  2n  N*)

在数学上,也可以给出归纳方法定义:

f(n) = 0  (n=0)
f(n) = 1  (n=1)
f(n) = f(n - 1) + f(n - 2)    (n  2n  N*)

很明显这是一个分段函数,可以用二分支来实现:

In [1]:
def fibonacci(n):
    """定义递归函数,接收非负整数n,返回斐波那契数列的第n项"""
    if n == 0:  # 第一项为1
        return 0
    elif n == 1:  # 第二项为1
        return 1
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)  # 其他项等于前两项之和


num = int(input())     # 输入非负整数
print(fibonacci(num))  # 调用递归函数求斐波那契数列的第n项并输出
5

需要注意的是,在else分支下面,程序的返回值中再次调用了函数fibonacci(),只是参数值变小了。

修改一下描述:

f(n) = n  (n1)
f(n) = f(n - 1) + f(n - 2)    (n  2n  N*)
In [4]:
def fibonacci(n):
    """定义递归函数,接收非负整数n,返回斐波那契数列的第n项"""
    if n <= 1:  # 前两项为0或1
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)  # 其他项等于前两项之和


num = int(input())     # 输入非负整数
print(fibonacci(num))  # 调用递归函数求斐波那契数列的第n项并输出
-7

类似的问题还有:

2.阶乘计算:递归也常用于计算阶乘。

In [ ]:
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

3.汉诺塔问题:汉诺塔问题是一个经典的递归问题。

In [6]:
def hanoi(n, source, target, auxiliary):
    if n > 0:
        hanoi(n - 1, source, auxiliary, target)
        print('Move disk from {} to {}'.format(source, target))
        hanoi(n - 1, auxiliary, target, source)

4."五人分鱼"。问题描述如下:五个人在夜间捕到一堆鱼,约定第二天分鱼。第二天早上,第一个人醒来,将鱼分为五份,发现多了一条,就扔掉这条,拿走自己的一份。后来第二个、第三个、第四个、第五个人都是这样分鱼的。问他们至少捕到多少条鱼?

In [7]:
def fish(n, total):
    # n是还未分鱼的人数,total是当前的鱼数
    if n == 0:  # 所有的人都已经分过鱼了
        return total  # 返回最后的鱼数
    elif total % 5 == 1:  # 当前的鱼数可以被5整除,并且还剩一条鱼
        # 递归调用fish函数,n减1,total更新为剩下的鱼数
        return fish(n - 1, (total - 1) * 4 / 5)
    else:  # 当前的鱼数不能被5整除,或者不剩一条鱼
        return False  # 返回False,表示这个鱼数不符合条件

total = 1  # 从1开始尝试
while True:  # 不断尝试,直到找到一个符合条件的鱼数
    if fish(5, total):  # 如果这个鱼数符合条件
        print(f"五人至少合伙捕到{total}条鱼")  # 输出这个鱼数
        break  # 结束循环
    total += 1  # 尝试下一个鱼数

5.有一座八层宝塔,每一层都有一些琉璃灯,每一层的灯数都是上一层的二倍,已知共有765盏琉璃灯,求解每层各有多少。

In [1]:
def lamps(n, total):
    if n == 1:  # 如果只有一层
        return [total]  # 返回总灯数
    else:
        # 第一层的灯数是总灯数除以(2^n - 1)
        first = total // (2 ** n - 1)
        # 返回第一层的灯数和剩下层的灯数
        return [first] + lamps(n - 1, total - first)


print(lamps(8, 765))  # 输出每层的灯数
[3, 6, 12, 24, 48, 96, 192, 384]

6.猴子吃桃问题:猴子摘下了n个桃子,第一天吃了一半又多吃了一个,第二天也是吃了剩下的一半又多吃了一个,到了第十天想再吃时,发现只剩下一个桃子了。问猴子第一天共摘了多少个桃子?

In [ ]:
def peaches(day):
    if day == 10:  # 第10天只剩下一个桃子
        return 1
    else:
        # 第day天的桃子数是第day+1天的桃子数加一后的两倍
        return (peaches(day + 1) + 1) * 2

print(peaches(1))  # 输出第一天的桃子数

__递归(Recursion)__是指在函数的定义中__调用函数自身__的方法。

递归的基本思想是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个,所以就产生了函数调用它自身的情况。当然这样的函数必须有明显的结束条件,__以避免产生无限递归的问题__

函数内部可以调用其他函数。如果一个函数内部__调用函数本身__,这个函数就是__递归函数__

递归的解题思路十分擅长解决除了规模大小不同,其他完全一样的问题。比如阶乘,Fibonacci数列等问题,用递归方法来解决,可以用较少的代码来完成。

In [3]:
def fact(n):
    '''定义递归函数,接收非负整数n,返回n的阶乘'''
    if n == 0:      # 必须有一个终止条件
        return 1    # 此时返回值确定值
    else:
        return n * fact(n - 1)  # 调用fact()自身,即当前数n的阶乘等于n与前一个数n-1的阶乘之积

num = int(input())  #输入非负整数
print(fact(num))    #调用递归函数求阶乘并输出

函数fact(5)开始调用时,传入参数为5,返回值为5 fact(4),即调用fact()函数自身,传入值为4,返回值为4 fact(3)....,以此类推,函数每调用一次,问题的__规模减小1__,直至遇到函数的__结束条件__n == 0,终止函数的调用,再__逐层返回__,逆向运算得到结果。

下图说明了递归求解的过程:

__递归函数必须满足如下特性:__

  1. 必须有一个明确的递归终止条件。 递归在有限次调用后要进行回溯才能得到最终的结果,那么必然应该有一个明确的临界点,程序一旦到达了这个临界点,就不用继续函数的调用而开始回溯,该临界点可以防止无限递归。
  2. 给出递归终止时的处理办法。 在递归的临界点应该直接给出问题的解决方案。
  3. 每次进入更深一层递归时,问题规模相比上次递归都应有所减少或更接近于解。

__能用递归解决的问题__必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,需要抽象出一个简单的重复的逻辑,以便使用相同的方式解决子问题。

__递归函数的优点__是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。__递归的缺点__是效率不高,而且使用递归函数需要注意防止栈溢出。

在计算机中,函数调用是通过__栈(stack)__这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致__栈溢出(stack overflow)__。一般默认递归长度在1000左右,使用Python写的递归程序如果递归太深, 那么极有可能因为超过系统默认的递归深度限制而出现错误。

RuntimeError: maximum recursion depth exceeded in comparison

解决这个问题有三种方法:

  1. 人为设置递归深度,方法如下:
In [ ]:
import sys


sys.setrecursionlimit(3000)   #括号中的值为递归深度

递归深度同时受操作系统栈的深度限制,不同系统环境下,支持的最大递归深度不同。在64位windows 10环境下,最大递归深度约为3900次左右。

  1. 递归改成非递归,用循环的方法实现。
  1. 尾递归优化。

尾递归优化是解决递归栈溢出的一种方法。 所谓尾递归是指在函数返回的时候,调用自身,并且return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

在传统的递归中,典型的模式是,执行第一个递归调用,然后接着调用下一个递归来计算结果。这种方式中途是得不到计算结果,直到所有的递归调用都返回。这样虽然很大程度上简洁了代码编写,但是随着递归的深入,之前的一些变量需要分配堆栈来保存,会导致效率降低。

相对传统递归,尾递归是一种特例。在尾递归中,先执行某部分的计算,然后开始调用递归,所以可以得到当前的计算结果,而这个结果也将作为参数传入下一次递归。这也就是说函数调用出现在调用者函数的尾部,所以其有一个优越于传统递归之处,就是无需去保存任何局部变量,减少内存消耗,提高性能。当前时刻的计算值作为第二个参数传入下一个递归,使得系统不再需要保留之前计算结果。理论上讲,没有产生中间变量来保存状态的尾递归,完全可以复用同一个栈帧来实现所有的递归函数操作。这种形式的代码优化就叫做尾递归优化。但是遗憾的是Python本身不支持尾递归(没有对尾递归做优化),而且对递归的次数有限制,当递归深度超过1000时,同样会抛出异常。

In [ ]: