《从问题到程序:用Python学编程和计算》——3.2 递归

简介:

本节书摘来自华章计算机《从问题到程序:用Python学编程和计算》一书中的第3章,第3.2节,作者 裘宗燕,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

3.2 递归

有一种函数定义比较特殊,就是在定义的函数体里调用被定义的函数自身。Python允许这种形式的函数定义,称为递归定义,这样定义出的函数也经常被称为递归函数。但是,这样做带来了一个数学里经常提到的问题:基于自己定义自己,这种形式的定义有意义吗?会不会做出了一个所谓的“循环定义”,完全没有意义?本节讨论这方面的一些情况,并说明递归定义是一种很有用的编程技术。

3.2.1 递归定义的函数

image
image

3.2.2 乘幂的计算

image
image
image
image

3.2.3 循环和递归

前面讨论过循环程序与简单程序(直线型程序和分支程序)之间的不同。上面又提出了递归定义的函数,在其写法中并没有出现循环结构,但同样可能导致重复的计算。这里的情况是通过递归调用,多次重复执行同一个函数的体代码。由此我们可以想一个问题:递归和循环能做的事情类似,那么,两者可以相互替代吗?

人们对这方面的问题做过一些理论研究。一个结果是:如果一个语言中包含了基本语句、顺序组合和条件语句,而且允许以递归的方式定义函数,这样构成的语言就足够强大了,足以描述所有可能的计算过程。另一方面,一个包含了基本语句、顺序组合和循环结构的语言也足够强大,也可以写出所有可能的计算。

进一步说,不难设计出一种方法,采用这种方法,可以根据任何一个循环定义出一个递归函数,在程序里原来写循环的地方调用这个函数,就能得到同样的计算效果。而反过来的情况比较复杂,要把一个递归定义的函数改为不用递归方式定义的函数(称为原函数的非递归实现),有时会遇到一些困难,但也是可以完成的。

上面的理论结果说明递归和循环中有一个就足够了。但为了方便实际的程序开发,各种实际编程语言都引进了一些冗余的机制,包括冗余的控制机制(以及后面将会讨论的数据机制)。例如,Python语言有循环结构,同时也允许递归的函数定义。

作为例子,现在考虑前一小节的乘幂函数,看看怎样做出与之对应的循环实现。

可以看到,在前面power函数的递归实现里,递归调用之间传递两个参数x和n,还有一个是函数的返回值。虽然传递的参数名字不变,但参数的值一直在变化。其中变量n的值不断减小(或者整除2或者减一),x不断求平方。每次遇到指数为奇数时,就是当时x的值作为乘数。按说需要用一个变量记录这个变化的x。但由于Python中函数的参数就是变量,可以直接用参数x记录这个变化的值。另外,为了反映积累的乘积(递归函数的返回值),引进另一个变量p。由于这里用乘法累积信息,p的初值设为1。
根据这些考虑,写出的函数定义如下:

def power(x, n):
    p = 1
    while n > 0:
        if n % 2 != 0:
            p *= x
            n -= 1
        else:
            x *= x
            n //= 2
    return p

p是这里的累积变量,循环中把x的一些幂次累积到p上,最终得到x的n次幂。循环里区分了几种情况:指数n等于0时累积工作完成,循环结束,变量p的值即为所需。循环体就是一个if语句,两个分支分别处理指数为偶数和奇数的情况,其中用了几个扩展的赋值语句更新变量。注意,对p的初始化保证,即使这个循环的体一次也没有执行,得到的结果也是对的。

3.2.4 斐波那契数列的计算

image
image
image
image
image
image
image
image
image
image
image

3.2.5 最大公约数

最大公约数(Greatest Common Divisor,简写为GCD或gcd)是一个重要的数学概念。整数n的约数就是能整除n的整数,k整除n即为n除以k的余数为0。显然,对任意的n,1和n本身都是其约数。两个整数m和n的最大公约数,就是能同时整除两者的整数中绝对值最大的那个数。由于1是任何整数的约数,两个整数的最大公约数一定存在。本节考虑最大公约数的计算问题,定义完成它的Python函数。这个问题也存在许多求解方法。

朴素解法

最直接的方法是系统化地检查一些整数,直至找到能同时整除两个数的最大数。设函数的参数分别是m和n,为做顺序检查,需要用一个辅助变量k,用它的值做检查。下面的问题是k如何取值。根据前面的经验,最简单的系统化检查方法就是顺序检查,从某个值开始逐一试验,直至到达我们的目标。为此需要设计一个循环,实现这种重复检查。这样,k的取值问题就变成如何取初值,如何更新,如何判断结束。

方法1:令k从1开始递增取值,结束条件是k大于m或n中的某一个。这样就能保证得到结果,因为最大公约数绝不会大于两个数中的任何一个。

下一个问题是如何得到所需结果。m和n可能有许多公约数,变量k最后的值也不会是m和n的公约数,因为它已大于两个数之一。这些情况说明,需要在循环中记录遇到的公约数,为此需要引进新变量。对这个具体问题,只需要引进一个变量,记录已经找到的最大的公约数。例如用变量d,其初值可以设为1,因为1是任何整数的约数。遇到m和n的新公约数时把它记入d,这个语句应该是:

if m % k == 0 and n % k == 0:
    d = k;  # k是新的公约数,而且更大

可以看到,引入d及其初值后,k的取值可以从2开始。函数定义已经很清楚:

def gcd (m, n):
    d = 1
    k = 2
    while k <= m and k <= n:
        if m % k == 0 and n % k == 0:
            d = k
        k += 1
    return d

这里假定了m和n的值都不小于0。如果m和n没有除去1之外的其他公约数,变量d的初值1就会留下来,也能得到正确结果。

如果考虑任意整数参数,还需要仔细考虑各种情况:

image

综合起来,在函数的循环之前应该加上几个语句:

    if m == 0:
        return n
    if n == 0:
        return m
    if m < 0:
        m = -m
    if n < 0:
        n = -n

两个参数都是0的情况也自然地处理了,无须专门检查。

在这个示例程序里用了一个辅助变量d,用于记录计算过程中得到的中间结果。根据需要引入辅助变量是程序设计中的一种常用技术。另一个问题也值得注意:许多计算问题都有一些需要处理的特殊情况,应当仔细分析,分别给以处理。其中一件事情就是分析参数的各种可能情况,看看程序是否都能给出正确结果,必要时修改程序,经常需要在主要代码前面加一些条件判断,也可能需要加入另外的处理语句。

方法2:令k从某个大数开始递减取值,这样,找到的第一个公约数就是最大公约数。可以令k的初值为m和n中较小的一个,在没有遇到公约数的情况下递减。结束条件是:或者k值达到1,或者已经找到了公约数。由于1总是两个数的公约数,因此前一条件被后一条件覆盖。有关程序留作读者的练习。

上面这两种求解方法的共同点是采用重复测试的方法,用每一个数去检查条件是否成立。这里采用的是通用的生成与检查方法,很多问题都能这样求解。为应用这种方法,需要设计并生成需要检查的数据集,实现适当检查的方法。这种方法的缺点是效率通常比较低,如果需要检查的数据集合很大,程序中的循环就要反复执行很多次。一般而言,如果能找到解决具体问题的特殊方法,就不应该选用这种“通用”方法。

辗转相除法

对于求最大公约数的问题,古代科学家已经提出了更有效的计算方法,这就是著名的欧几里得算法(欧氏算法),即辗转相除法。下面介绍欧几里得算法的程序实现。首先,不难用递归形式给出辗转相除法求最大公约数的定义:

image

这里假定m和n都是自然数,而且m不为0。

函数定义1(递归定义):先假定函数的第一个参数非0,而且两个参数都不小于0。下面的函数定义直接对应于辗转相除法的数学定义:

def gcd(m, n):
    if n % m == 0:
        return m
    return gcd(n % m, m)

这个函数很简单,对欧几里得算法的研究保证该函数的计算一定能结束。这个函数的计算速度很快,快于前面的顺序检查,参数较大时优势更明显。

为处理各种特殊情况,可以另外写一个函数,其中调用上面的函数。这也是程序设计中常见的技术。下面是求最大公约数的主函数:

def gcd_main(m, n):
    if m < 0:
        m = -m
    if n < 0:
        n = -n
    if m == 0:
        return n
    return gcd(m, n)

函数定义2(循环方式):辗转相除也是一种重复性工作,其中反复求余数。前面用递归的方式定义函数,也可以通过循环实现。在写程序前,先分析一下应当如何写循环:

1)循环开始时,m和n是求最大公约数的出发点。

2)每次循环判断n % m是否为0。如果为0,m就是最大公约数,结束。

3)否则做变量更新:更新后的n取原来m的值,而m取原来n % m的值。这两个变量更新可以用一个平行赋值实现:

m, n = n % m, m

下面是这个函数的一种定义,其中假定两个参数的值均不小于0。把这一函数补充完整的工作留给读者考虑。

def gcd(m, n):
    if m == 0:
        return n
    while n % m != 0:
        m, n = n % m, m
    return m

n的值为0以及m和n的值都为0的情况没有特殊处理,因为这些处理都已经包含在定义里了。读者很容易弄清楚这一点。上面函数里也有一个较复杂的循环。按照前面说法,一个循环中总要维持某种不变关系,以保证循环的正确性。那么,上面函数里的这个循环维持的是变量之间的什么关系呢?这一点也请读者考虑。

这个例子也说明了平行赋值的作用。在上面的函数里采用了平行赋值,既简单又清晰,意义也正确。如果将其分开写为两个单独的赋值语句,无论怎样排序都不能得到正确结果。当然,这并不说明用简单赋值不能解决这个问题,但要完成同样功能,需要另外引进一个辅助变量。请读者自己验证这些论断。

3.2.6 不容易用循环求解的递归问题

image
image
image
image
image
image
image
image

3.2.7 更复杂的递归情况

前面讨论中出现的递归都是一个函数在体中调用自身,这种递归可以称为自递归。有时也会出现需要两个或多个函数相互调用,形成复杂的递归情况。其中最简单的情况就是两个函数相互递归调用,例如在f里调用g,在g里调用f。

如果需要定义两个相互调用的函数,就会遇到一个问题:无论先定义哪个,其函数体里总需要调用另一个尚未定义的函数。还好,在Python语言里,怎么排列两个函数定义都没问题。在处理函数的定义时,Python解释器并不检查其中调用的函数有无定义,只要在实际调用时被调函数有定义,能找到它,就万事大吉了。如果执行到一个函数调用时被调用函数还没有定义,Python解释器自然要报错。

下面是两个相互递归的函数,它们分别判断参数的奇偶:

def even(n):
    if n == 0:
        return True
    else:
        return odd(n-1)

#even(4) # 在这里调用将会报销

def odd(n):
    if n == 0:
        return False
    else:
        return even(n-1)

如果在函数even的定义之后调用even(4),系统就会报错。因为当解释器处理到这一点,函数odd还没有定义。在odd有定义后,写even(4)就没问题了。

这个例子本身并没有实际意义,只是用来说明情况。在实际编程中,可以出现更复杂的递归情况,这里就不举例了。

相关文章
|
14天前
|
机器学习/深度学习 存储 设计模式
Python 高级编程与实战:深入理解性能优化与调试技巧
本文深入探讨了Python的性能优化与调试技巧,涵盖profiling、caching、Cython等优化工具,以及pdb、logging、assert等调试方法。通过实战项目,如优化斐波那契数列计算和调试Web应用,帮助读者掌握这些技术,提升编程效率。附有进一步学习资源,助力读者深入学习。
|
14天前
|
机器学习/深度学习 数据可视化 TensorFlow
Python 高级编程与实战:深入理解数据科学与机器学习
本文深入探讨了Python在数据科学与机器学习中的应用,介绍了pandas、numpy、matplotlib等数据科学工具,以及scikit-learn、tensorflow、keras等机器学习库。通过实战项目,如数据可视化和鸢尾花数据集分类,帮助读者掌握这些技术。最后提供了进一步学习资源,助力提升Python编程技能。
|
2天前
|
Python
[oeasy]python074_ai辅助编程_水果程序_fruits_apple_banana_加法_python之禅
本文回顾了从模块导入变量和函数的方法,并通过一个求和程序实例,讲解了Python中输入处理、类型转换及异常处理的应用。重点分析了“明了胜于晦涩”(Explicit is better than implicit)的Python之禅理念,强调代码应清晰明确。最后总结了加法运算程序的实现过程,并预告后续内容将深入探讨变量类型的隐式与显式问题。附有相关资源链接供进一步学习。
15 4
|
14天前
|
设计模式 机器学习/深度学习 前端开发
Python 高级编程与实战:深入理解设计模式与软件架构
本文深入探讨了Python中的设计模式与软件架构,涵盖单例、工厂、观察者模式及MVC、微服务架构,并通过实战项目如插件系统和Web应用帮助读者掌握这些技术。文章提供了代码示例,便于理解和实践。最后推荐了进一步学习的资源,助力提升Python编程技能。
|
15天前
|
数据采集 搜索推荐 C语言
Python 高级编程与实战:深入理解性能优化与调试技巧
本文深入探讨了Python的性能优化和调试技巧,涵盖使用内置函数、列表推导式、生成器、`cProfile`、`numpy`等优化手段,以及`print`、`assert`、`pdb`和`logging`等调试方法。通过实战项目如优化排序算法和日志记录的Web爬虫,帮助你编写高效稳定的Python程序。
|
4天前
|
Java API Docker
在线编程实现!如何在Java后端通过DockerClient操作Docker生成python环境
以上内容是一个简单的实现在Java后端中通过DockerClient操作Docker生成python环境并执行代码,最后销毁的案例全过程,也是实现一个简单的在线编程后端API的完整流程,你可以在此基础上添加额外的辅助功能,比如上传文件、编辑文件、查阅文件、自定义安装等功能。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
在线编程实现!如何在Java后端通过DockerClient操作Docker生成python环境
|
12天前
|
机器学习/深度学习 设计模式 API
Python 高级编程与实战:构建 RESTful API
本文深入探讨了使用 Python 构建 RESTful API 的方法,涵盖 Flask、Django REST Framework 和 FastAPI 三个主流框架。通过实战项目示例,详细讲解了如何处理 GET、POST 请求,并返回相应数据。学习这些技术将帮助你掌握构建高效、可靠的 Web API。
|
12天前
|
机器学习/深度学习 设计模式 测试技术
Python 高级编程与实战:构建自动化测试框架
本文深入探讨了Python中的自动化测试框架,包括unittest、pytest和nose2,并通过实战项目帮助读者掌握这些技术。文中详细介绍了各框架的基本用法和示例代码,助力开发者快速验证代码正确性,减少手动测试工作量。学习资源推荐包括Python官方文档及Real Python等网站。
|
15天前
|
数据采集 人工智能 数据挖掘
Python 编程基础与实战:从入门到精通
本文介绍Python编程语言,涵盖基础语法、进阶特性及实战项目。从变量、数据类型、运算符、控制结构到函数、列表、字典等基础知识,再到列表推导式、生成器、装饰器和面向对象编程等高级特性,逐步深入。同时,通过简单计算器和Web爬虫两个实战项目,帮助读者掌握Python的应用技巧。最后,提供进一步学习资源,助你在Python编程领域不断进步。
|
15天前
|
Python
Python 高级编程与实战:深入理解面向对象与并发编程
本文深入探讨Python的高级特性,涵盖面向对象编程(继承、多态、特殊方法、类与实例属性)、异常处理(try-except、finally)和并发编程(多线程、多进程、异步编程)。通过实战项目如聊天服务器和异步文件下载器,帮助读者掌握这些技术,编写更复杂高效的Python程序。

热门文章

最新文章