乐读窝

程序员的数学思维修炼

乐读窝 > 科普学习 > 程序员的数学思维修炼

第3章 递归——自己调用自己

书籍名:《程序员的数学思维修炼》    作者:周颖



递归(Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中又调用函数自身的方法。递归是一种奇妙的思考问题的方法,通过递归的这种思路,可简化问题的定义。



3.1 从前有座山,山里有座庙


递归一词常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。



3.1.1 老和尚讲的故事


将时光往前推,在中国还流传着这样一个有趣的故事:

从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?‘从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……’”

怎么样?这个故事就是不断重复着老和尚讲的故事,有趣?还是无聊?

不管怎样,这个故事可以不断地重复,一直讲下去。

这就是生活中一个用递归形成的故事。



3.1.2 德罗斯特效应


再来看一张图,如图3-1所示为一张网页的截图,在网页图中又包括相同的一份较小的截图,在小图中又包含一份更小的截图,……,这样就形成了一幅递归形式的图形。

图3-1

图3-1所示的图形称为德罗斯特效应(Droste  effect),是递归的一种视觉形式,是指一张图片的某个部分与整张图片相同,如此产生无限循环。这种照片是通过名为Mathmap的数学软件制作出来,使用PhotoShop的Droste  Effect滤镜也可制作出这种效果。



3.1.3 什么是递归


那么,什么是递归呢?

在数学和计算机科学中,递归指由一种(或多种)简单的基本情况定义的一类对象或方法,并规定其他所有情况都能被还原为其基本情况。

例如,我们人类的发展繁衍中,人之间的辈份就是一种递归(如图3-2所示),在这个递归中首先定义一个基本情况,接着递归定义,具体情况如下:

图3-2

A的父母是A的祖先(这是基本情况)。

A祖先的双亲同样是A的祖先(递归步骤)。

对于递归,一种便于理解的心理模型,认为递归对对象的定义是按照“先前定义的”同类对象来定义的。

例如:要求能移动100个箱子,该怎么移动?

首先移动1个箱子,并记下它移动到的位置,然后再去解决较小的问题(由于已移动了1个箱子,剩下99个箱子),这时的问题就简化为“怎样才能移动99个箱子?”,……不断重复,到最后,问题将简化为“怎样移动1个箱子”,如图3-3所示。

图3-3

类似的定义在数学中十分常见。例如,集合论对自然数的正式定义是:1是一个自然数,每个自然数都有一个后继,这一个后继也是自然数,如图3-4所示。

图3-4



3.1.4 用递归能解决哪些问题


递归是一种非常接近自然思维的思想,其实了解多了以后,用起递归来是非常自然的,但不是每个场合使用递归都是合适的。通常递归方法适合于层次结构本身就是递归定义的情况,比如二叉树的遍历,因为二叉树的定义就是“一颗空树,或者一个节点+左右两颗子二叉树”,它的定义就是递归的,所以用递归操作相当方便。

简单来说,递归问题,可以划分为一个或多个子问题,而处理子问题的规则与处理原问题的规则是一样的。

如图3-2所示的祖先问题、用集合论对自然数的定义问题,以及老和尚讲的故事,都是无穷无尽的。通常意义上来说,对于一个无穷尽的问题,是没办法编写程序来解决的,因为程序没有办法结束。

虽然常见的递归问题都没有尽头,不过,我们可以找到起点,这时可从某一个指定的终点开始,向起点方向运行,当到达起点位置时,即可结束递归调用。

那么,在实际应用中要使用递归算法,通常需要分析以下3方面的问题:

每一次递归调用,在处理问题的规模上都应有所缩小(通常问题规模可减半)。

相邻两次递归调用之间有紧密的联系,前一次要为后一次递归调用做准备,通常是前一次递归调用的输出作为后一次递归调用的输入。

在问题的规模极小时,必须直接给出解答而不再进行递归调用,因而每次递归调用都是有条件的(以规模未达到直接解答的大小为条件),无条件递归调用将会成为死循环而不能正常结束。

根据上面的描述,在设计递归算法时,主要需考虑以下两方面的问题:

确定递归公式。把规模大的、较难解决的问题变成规模较小、易解决的同一问题,需要通过哪些步骤或等式来实现?这是解决递归问题的难点。

确定边界(终了)条件。在什么情况下可以直接得出问题的解?这就是问题的边界条件及边界值。



3.1.5 一个简单例子:求最大公约数


求最大公约数是数学中一个简单的问题。

如果有一个自然数A能被自然数B整除,则称A为B的倍数,B为A的约数。几个自然数公有的约数,叫做这几个自然数的公约数。这些公约数中最大的一个公约数,称为这几个自然数的最大公约数,简称为GCD。

例如,在2、4、6这3个数中,2就是2、4、6的最大公约数。

怎么求最大公约数呢?早在公元前300年左右,欧几里得就在他的著作《几何原本》中给出了高效的解法——辗转相除法。

辗转相除法的方法是用较大的数M除以较小的数N,较小的除数N和得出的余数R构成新的一对数,继续重复前面的除法(用较大数除以较小数),直到出现能够整除的两个数,其中较小的数(即除数)就是最大公约数。

例如,求153和123的最大公约数,操作过程如下:

所以,153和123的最大公约数就是3。

可以看出,在用辗转相除法求最大公约数时,每次相除后的除数N和余数R都将原来的问题规模缩小,而且有一个边界条件(两数能够整除,即余数为0)。有这两条,就可使用递归算法来解这个问题,处理流程如图3-5所示。

图3-5

根据图3-5所示的流程,可编写出以下C语言程序。

在以上程序中,定义了一个gcd()函数,在这个函数中调用gcd()函数,这样就形成了递归调用。在这个函数内部通过辗转相除,然后判断余数是否为0,若为0就返回n值,否则递归调用gcd()函数进行辗转相除。

怎么样,递归调用函数是不是很简单呢?