目录
5.1 递归的基本概念
一个概念的定义中用到了这个概念本身,这就叫递归。例如,假定有个概念叫“堆乘”,用如下两句话定义“n的堆乘”(不妨记为“n#”),就是递归:
(1)“n的堆乘”就是n乘以“(n - 1)的堆乘”;
(2)“1的堆乘”是1。
第1句中,解释“堆乘”这个词的时候用到了“堆乘”这个词,貌似没完没了的循环定义,让人搞不明白。如果没有第2句,那确实如此。有了第2句,n的堆乘到底是什么,就可以由1的堆乘是1,逐步递推出来:4#=4×3#、3#=3×2#、2#=2×1#、1#=1,倒推回去,可以得到4#=4×3×2×1。原来堆乘就是阶乘。第2句话使得面对“1的堆乘是什么”这样的问题时,不必再用让人搞不懂的“1的堆乘等于1乘以0的堆乘”来回答,而是直接得到答案1,因此第2句可以称为递归的“终止条件”。
在程序设计中,一个函数自己调用了自己,就称为递归。其实函数调用自己,和调用别的函数并无本质区别,完全可以看作是调用了另一个同功能函数。调用自己的函数,称为递归函数。下面是一个求n阶乘(n>=1)的递归函数:
def F(n): #函数返回n的阶乘
if n == 1: #终止条件
return 1
return n * F(n-1)
print(F(4)) #>>24
print(F(5)) #>>120
递归函数是如何执行的,初学者往往难以理解。图5.1.1演示了F(4)的计算过程(从F(4)开始顺着箭头方向看)。
算F(4)时,进入F函数,此刻n=4。要算F(4),就要先算F(3),于是再次进入F函数,此刻n=3。F(4)算是第一层函数调用,F(3)就是第二层。每一层调用的n的值不同,不会互相影响。将调用自己看作调用另一个同功能的函数,即可很自然理解这一点。整个执行过程还可以描述如下:
F(4)2->F(4)4->F(3)2->F(3)4->F(2)2->F(2)4->F(1)2->F(1)3:返回1->
F(2)4:返回2*1->F(3)4:返回3*2->F(4)4:返回4*6->函数执行结束
上面的F(i)j表示在n=i那一层的函数调用中,执行第j行。最先执行的是F(4)2,表示在n=4的那一层函数调用中,先执行第2行“ifn==1:”。接下来执行F(4)4,第4行在执行的过程中进入下一层函数调用,下一层函数调用中n=3,所以F(4)4后面被执行的就是F(3)2,再接下来是F(3)4……执行到F(1)3后,函数开始逐层向上返回,先返回到F(2)4,把n=2时的第4行执行完毕,返回值是2*F(1),即1*1,返回到F(3)4,F(3)4执行完则返回值为3*F(2),即3*2=6,并且返回到F(4)4,F(4)4返回4*6,函数调用结束。
由此可见,递归函数一定要有一个终止递归的条件,满足此条件时,函数就返回,不再调用自身。否则,递归就会没完没了地进行下去。无休止的递归会导致“栈溢出”而使得程序崩溃。有时程序中没有死循环,然而却总是不能结束,就要考虑是否发生了无限递归。
上面F函数的终止条件,就是n==1。
求斐波那契数列的第n项,也可以用递归的办法完成:
def Fib(n): #求斐波那契数列第n项
if n == 1 or n == 2:
return 1
else:
return Fib(n-1)+Fib(n-2) #第n项等于第n-1项和第n-2项之和
print(Fib(6)) #>>8
但是这个递归的做法,存在大量重复计算,例如算fib(5)时会把fib(4)从头到尾算一遍,算fib(6)时又要把fib(4)从头到尾算一遍……因此其计算速度远远慢于前面的循环解法,用个人计算机来算,十万年未必能算出第100项,只能用来演示一下递归的思想。
递归可以用来替代循环。假设有函数g(),下面的循环:
for i in range(4):
g()
可以用调用一个递归函数来替代:
defcall(f,n):#参数f是函数ifn>0:f()#调用f代表的函数call(f,n-1)call(g,4)
程序设计语言中有递归,就可以不需要循环。有的语言,比如LISP,就是如此。有了循环,其实也可以不需要递归,只是不够方便。当然大多数程序设计语言都是同时支持递归和循环的。
递归和循环可以互相替代这件事情有点深奥,非计算机专业的读者可以不必深究。
默认情况下,Python递归函数最多只能执行大约1000层,就会导致栈溢出的RuntimeError。可以用sys.setrecursionlimit()函数来增加最大递归深度,请读者自行查阅相关介绍。但是,这个函数也不是非常好使,Python相比其他语言更容易因递归太深导致RE。
5.2 先做一步再递归:上台阶问题
5.3 问题分解:汉诺塔问题
5.4 递归替代循环:N 皇后问题
注:5.2,5.3,5.4 这三个利用递归解决的实际问题,详见【Python习题】感悟&易错点总结
5.5 递归绘制分形图案:绘制雪花曲线
要进行绘图,可以使用Python自带的turtle库。turtle库中有许多函数支持绘图,用法是turtle.xxx(...),xxx是函数名。绘图是在一个窗口中进行的,用turtle.setup(x,y)可以创建一个宽x像素,高y像素的窗口,窗口会出现在屏幕中央。窗口是一个平面直角坐标系,窗口的中心位置是坐标系原点,即其坐标是(0,0)。这个坐标系有方向的概念,方向用角度来表示。正东方向是0度,正北方向是90度,正西方向是180度,正南方向是270度。当然也可以说正南方向是-90度,正西方向是−180度。
不妨把turtle创建的窗口想象成一张纸。这张纸上有一支虚拟的笔。笔开始的位置是在(0,0),且笔是落在纸上的。当笔落在纸上移动时,就会画出线条。笔是有前进方向的。笔的初始方向是0度。turtle.fd(x)会使笔沿着前进方向移动x像素。turtle.left(x)会使得笔的方向左转x度,turtle.right(x)会使得笔的方向右转x度。
下面要在窗口上绘制雪花曲线。雪花曲线也称为科赫曲线,其递归定义如下:
(1)长为size,方向为x(x是角度)的0阶雪花曲线,是沿方向x绘制的一根长为size的线段。
(2)长为size,方向为x的n阶雪花曲线,由以下四部分依次拼接组成。
①长为size/3,方向为x的n−1阶雪花曲线。
②长为size/3,方向为x+60的n−1阶雪花曲线。
③长为size/3,方向为x−60的n−1阶雪花曲线。
④长为size/3,方向为x的n−1阶雪花曲线。
图5.5.1~图5.5.3是几个雪花曲线的示意图。
绘制长度为600像素,方向为0度的3阶雪花曲线的程序如下:
import turtle #画图要用这个turtle库
def snow(n,size):
#从笔的当前位置出发,在笔的当前方向画一个长度为size的n阶的雪花曲线
if n == 0: #0阶曲线
turtle.fd(size) #笔沿着当前方向前进size个像素
else:
for angle in [0,60,-120,60]:
turtle.left(angle) #笔左转angle度,用turtle.lt(angle)也可以
snow(n-1,size/3)
turtle.setup(800,600) #创建窗口
turtle.penup() #抬起笔,这样笔在移动时就不会在窗口上画线
turtle.goto(-300,0) #将笔移动到(-300,0)位置
turtle.pendown() #放下笔
turtle.pensize(3) #设置笔的粗度为3像素
snow(3,600) #绘制长度为600,阶为3的雪花曲线,方向为0度
turtle.done() #保持绘图窗口,无此则画完图窗口自动关闭
程序运行结果如图5.5.4所示。
第1行:本行的作用是将turtle库“引入”进来,这样后面的标识符“turtle”才有定义。
函数snow(n,size)的含义是,从笔的当前位置出发,沿着笔的当前方向,画一条长为size的n阶雪花曲线。
第5行:0阶雪花曲线就是一条长为size的线段,turtle.fd(size)的含义是,沿当前笔的方向前进size,画出一条长为size的线段。
第7行到第9行:按照雪花曲线的递归定义,一条笔的当前方向上的长为size的n阶雪花曲线,应该由4段长为size/3的n-1阶雪花曲线连接而成。这个循环就依次画出这四段。若笔当前方向是x,则这四段的方向依次是x,x+60,x-60,x。可以看出,若n-1阶雪花曲线画完时笔的方向不变(和开始画时一样),那么n阶雪花曲线画完时笔的方向也不变。再加上0阶雪花曲线画完时笔的方向是不变的,由数学归纳法可知任何阶数的雪花曲线,画完时笔的方向都和开始画时一样。连画4段n-1阶雪花曲线,需要在画完一段后修改笔的方向,再画下一段。修改笔的方向,可以通过让笔左转某个角度来进行。调用turtle.left(d),即可以让笔的方向左转d度。因此要依次画这四段方向为x,x+60,x-60,x的n-1阶雪花曲线,就可以让笔先左转0度(等于没转)画第一段,再左转60度画第二段,再右转120度(即左转−120度)画第三段,然后再左转60度回到最初的方向x画第四段。
有了绘制雪花曲线的函数snow后,就可以绘制一个完整的雪花,如图5.5.5所示。
可以看出,该雪花由方向依次是0度、240度、120度的三段3阶雪花曲线构成。绘图函数如下:
def snowPiece():
turtle.setup(800,800)
turtle.speed(1000) #设置绘画速度
turtle.penup()
turtle.goto(-200,100)
turtle.pendown()
turtle.pensize(2)
snow(3,400) #画0度雪花曲线
turtle.right(120) #右拐120度
snow(3,400) #画-120度(即240度)雪花曲线
turtle.right(120)snow(3,400) #画120度(即-240度)雪花曲线
turtle.done()
雪花曲线是一种分形图形。什么是分形图形,言传有点难。大概说来,一个图形,由和整体图形相似的n个局部构成,每个局部又是由n个更小的和整体图形相似的局部构成……这样的图形就是分形图形。当然这是个非常不精确的模糊定义,请读者自己意会。