什么是递归
递归是一种解决问题的方法,将问题分解为更小的子问题,直到得到一个足够小的问题可以被很简单的解决。通常递归涉及函数调用自身。递归允许我们编写优雅的解决方案,解决可能很难编程的问题。
计算整数列表和
我们将以一个简单的问题开始,你已经知道如何不使用递归解决。 假设你想计算整数列表的总和。
def sumList(inputlist):
if len(inputlist) == 1:
return inputlist[0]
else:
return inputlist[0] + sumList(inputlist[1:])
print(sumList([1, 3, 5, 7]))
递归的三定律
像阿西莫夫机器人,所有递归算法必须服从三个重要的定律:
- 递归算法必须具有基本情况。
- 递归算法必须改变其状态并向基本情况靠近。
- 递归算法必须以递归方式调用自身。
整数转换为任意进制字符串
假设你想将一个整数转换为一个二进制和十六进制字符串。例如,将整数 10
转换为十进制字符串表示为 10
,或将其字符串表示为二进制 1010
。虽然有很多算法来解决这个问题,包括在栈部分讨论的算法,但递归的解决方法非常优雅。
def toStr(n, base):
converString = '0123456789ABCDEF'
if n < base:
return converString[n]
else:
return toStr(n // base, base) + converString[n % base]
print(toStr(33, 8))
栈帧:实现递归
假设不是将递归调用的结果与来自 convertString 的字符串拼接到 toStr,我们修改了算法,以便在进行递归调用之前将字符串入栈。
# 实现栈功能
class StackTest:
def __init__(self):
self.items = [] # 构造函数创建栈
def isEmpty(self):
return self.items == [] # 返回栈是否为空
def push(self, item):
return self.items.append(item) # 栈追加元素
def pop(self):
return self.items.pop() # 弹出最后一个元素
def peek(self):
return self.items[-1] # 返回最后一个元素
def size(self):
return len(self.items) # 返回栈的长度
def toStr(n, base):
convertString = '0123456789ABCDEF'
while n > 0:
if n < base:
rStack.push(convertString[n])
else:
rStack.push(convertString[n % base])
n = n // base
res = ''
while not rStack.isEmpty():
res = res + str(rStack.pop())
return res
if __name__ == '__main__':
rStack = StackTest()
print(toStr(10, 2))
可视化递归
我们将使用 turtle
模块递归绘制螺旋。
这个简单函数的基本情况是当我们想要绘制的线的长度(由 len 参数给出)减小到零或更小时。如果线的长度大于零,我们让点以 len 单位前进,然后向右转 90 度。当我们再次调用 drawSpiral
并缩短长度时递归。在ActiveCode 1 结束时,你会注意到我们调用函数 myWin.exitonclick()
,这是一个方便的缩小窗口的方法,使点进入等待模式,直到你单击窗口,然后程序清理并退出
import turtle
def drawSpiral(myTurtle, length):
if length > 0:
myTurtle.forward(length)
myTurtle.right(90)
drawSpiral(myTurtle, length-5)
if __name__ == '__main__':
myTurtle = turtle.Turtle()
myWin = turtle.Screen()
drawSpiral(myTurtle, 100)
myWin.exitonclick()
我们如何使用绘图来生成分形树。让我们更仔细地看一下代码。你会看到在第 5 行和第 7 行,我们正在进行递归调用。在第 5 行,我们在点向右转 20 度之后立即进行递归调用;这是上面提到的右树。然后在第 7 行,点进行另一个递归调用,但这一次后左转 40 度。点必须向左转 40 度的原因是,它需要撤消原来的向右转 20 度,然后再向左转 20 度,以绘制左树。还要注意,每次我们对树进行递归调用时,我们从 branchLen
参数中减去一些量; 这是为了确保递归树越来越小。你还应该看到到第 2 行的初始 if 语句是检查 branchLen
的基本情况大小。
import turtle
def tree(branchlen, t):
if branchlen > 5:
t.forward(branchlen)
t.right(20)
tree(branchlen - 15, t)
t.left(40)
tree(branchlen - 15, t)
t.right(20)
t.backward(branchlen)
def main():
t = turtle.Turtle()
myWin = turtle.Screen()
t.left(90)
t.up()
t.backward(100)
t.down()
t.color('green')
tree(75, t)
myWin.exitonclick()
if __name__ == '__main__':
main()
另一个展现自相似性的分形是谢尔宾斯基三角形。 谢尔宾斯基三角形阐明了三路递归算法。用手绘制谢尔宾斯基三角形的过程很简单。 从一个大三角形开始。通过连接每一边的中点,将这个大三角形分成四个新的三角形。忽略刚刚创建的中间三角形,对三个小三角形中的每一个应用相同的过程。 每次创建一组新的三角形时,都会将此过程递归应用于三个较小的角三角形。 如果你有足够的铅笔,你可以无限重复这个过程。
因为我们可以无限地应用算法,什么是基本情况? 我们将看到,基本情况被任意设置为我们想要将三角形划分成块的次数。有时我们把这个数字称为分形的“度”。 每次我们进行递归调用时,我们从度中减去 1,直到 0。当我们达到 0 度时,我们停止递归。
import turtle
# 绘制三角形及填充颜色
def drawTriangle(points, color, myTurtle):
myTurtle.fillcolor(color)
myTurtle.up()
myTurtle.goto(points[0][0], points[0][1])
myTurtle.down()
myTurtle.begin_fill()
myTurtle.goto(points[1][0], points[1][1])
myTurtle.goto(points[2][0], points[2][1])
myTurtle.goto(points[0][0], points[0][1])
myTurtle.end_fill()
def getMid(p1,p2):
return ( (p1[0]+p2[0]) / 2, (p1[1] + p2[1]) / 2)
def sierpinski(points,degree,myTurtle):
colormap = ['blue','red','green','white','yellow',
'violet','orange']
drawTriangle(points,colormap[degree],myTurtle)
if degree > 0:
# 左下
sierpinski([points[0],
getMid(points[0], points[1]),
getMid(points[0], points[2])],
degree-1, myTurtle)
# 中上
sierpinski([points[1],
getMid(points[0], points[1]),
getMid(points[1], points[2])],
degree-1, myTurtle)
# 右下
sierpinski([points[2],
getMid(points[2], points[1]),
getMid(points[0], points[2])],
degree-1, myTurtle)
def main():
myTurtle = turtle.Turtle()
myWin = turtle.Screen()
myPoints = [[-100, -50], [0, 100], [100, -50]]
sierpinski(myPoints, 3, myTurtle)
myWin.exitonclick()
if __name__ == '__main__':
main()