递归
4.递归
4.1 目标
- 要理解可能难以解决的复杂问题有一个简单的递归解决方案。
- 学习如何递归地写出程序。
- 理解和应用递归的三个定律。
- 将递归理解为一种迭代形式。
- 实现问题的递归公式化。
- 了解计算机系统如何实现递归。
4.2 什么是 递归
所谓递归
:就是大事化小,小事化了.对于一个大问题,给化成一系列可以重复操作的简单问题得一种方法.
通常递归涉及到函数调用自身,用循环解决的问题,递归都可以解决.
使用递归,可以让我们的程序变得更优雅简洁,逻辑上可读性更强.
4.3 计算整数列表和
如果需要计算前100个数的和,那么使用循环的方式是这样的:
def list_sum(num_list):
sum = 0
for i in num_list:
sum = sum + i
return sum
print(list_sum([i for i in range(1, 101)]))
但假设现在让你不用循环的方式实现.你会怎么做呢?这时,我们需要用到递归:
从1加到100可以分解成:1加上2~100的和.
那如何计算 2-100的和呢?我们可以分解成2+3~100的和.
以此类推…我们最后把问题分解成98+99~100的和.现在99+100不需要再循环了,加起来就行,
很好,我们已经把一个复杂的100位数加法问题分解成了一系列简单的2位数加法问题.
def list_sum(num_list):
if len(num_list) == 1:
return num_list[0]
else:
return num_list[0] + list_sum(num_list[1:])
print(list_sum([i for i in range(101)]))
好了,递归学完了.
???
what the hell???
你在逗我,怎么就结束了呢?
因为,剩下的交给计算机做就行了. 按照上述分解方式,当问题被分解到不能再分的时候,计算机就会返回这个基本值,到上一层去执行,而这一步也是两位数相加,相当于一个不可再分的基本问题,逐层向上,最终返回了我们想要的结果.
这么说可能有点抽象,我们 打个比方:分解问题就像向下坐一个超长滑梯,这个活我们是要花力气的,滑到底的时候我们耍够了,还想回去怎么办?我们可以坐电梯回去.而坐滑梯回去的过程就是计算机返回的过程,这个过程是计算机自动完成的,我们不需要操心,丝毫不费力气.
4.4 递归三定律
和阿西莫夫写的机器人三定律一样,所有的递归算法也必须服从这三个定律:
- 递归算法必须有基本情况
- 递归算法必须改变其状态并向基本情况接近
- 递归算法必须以递归方式调用自身
所谓基本情况
就是算法停止递归的条件:(99+100)
向基本情况靠近
是指问题越分解越小,最后小到我们定义的基本情况.
最后就是算法必须调用自身
,这是递归的定义.循环并不是递归.但循环能干的活,递归基本都能干!
== 学习递归最好的方式,就是放弃! ==
放弃钻进程序里面,挨个分析程序,这时候这个变量是什么值,那个时候变量又变成了别的什么东西…这样下去会给你弄疯!我们要做的就是相信,相信你的程序是对的,能完成任务,这样学习递归的过程就愉快多了!
4.5 整数转换为任意进制字符串
前面我们学了利用栈
这种数据结构来实现进制转换,其实仔细思考一下我们发现,进制转换其实也是一个递归的过程.
基本情况:转换一位数字取余成为一个字符串
接近步骤:除模取余.缩小范围
def toStr(num, base):
"""num是需要转换的十进制数字
base是需要转换的进制"""
convert_str = "012456789ABCDEF"
if num < base:
return convert_str[num]
else:
return toStr(num//base, base) + convert_str[num % base]
print(toStr(1452, 16))
4.6 栈帧:实现递归
当在 Python 中调用函数时,会分配一个栈来处理函数的局部变量。当函数返回时,返回值留在栈的顶部,以供调用函数访问。
栈帧还为函数使用的变量提供了一个作用域。 即使我们重复地调用相同的函数,每次调用都
会为函数本地的变量创建一个新的作用域
4.7 介绍:可视化递归
turtule模块.
简单使用:
import turtle
myTurle = turtle.Turtle() # 创建一个小乌龟对象
myWin = turtle.Screen() # 创建一个屏幕对象
def drawSpiral(myTurtle, lineLen):
'''绘制螺旋'''
if lineLen > 0:
myTurle.forward(lineLen) # 直行一段距离
myTurle.right(90) # 向右转弯
drawSpiral(myTurle, lineLen-5) # 递归调用自身
drawSpiral(myTurle, 100)
myWin.exitonclick() # 直到点击才退出
分形:分形来自数学的一个分支,并且与递归有很多共同之处。
分形的定义是,当你看着它时,无论你放大多少,分形有相同的基本形状。大自然的一些例子是大陆的海岸线,雪花,山脉,甚至树木或灌木。
这些自然现象中的许多的分形性质使得程序员能够为计算机生成的电影生成非常逼真的风景。
分形树的绘制:
import turtle
def tree(branchLen, t):
if branchLen > 5:
t.forward(branchLen)
t.right(20)
tree(branchLen - 10, t)
t.left(40)
tree(branchLen - 10, 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()
main()
4.8 谢尔宾斯基三角形
另一个展现自相似性的分形是谢尔宾斯基三角形。谢尔宾斯基三角形阐明了三路递归算法。
用手绘制谢尔宾斯基三角形的过程很简单。 从一个大三角形开始。通过连接每一边的中点,将这个大三角形分成四个新的三角形。忽略刚刚创建的中间三角形,对三个小三角形中的每一个应用相同的过程。 每次创建一组新的三角形时,都会将此过程递归应用于三个较小的角三角形。
degree代表度,即绘制三角形的次数.用于控制接近基本情况:连接三个点,绘制三角形
4.9 复杂递归问题
前面的章节是简单的递归问题, 这一节我们将看一些复杂的可迭代类型问题,但这些问题可以被递归算法优雅地解决.
4.10 汉诺塔
一共只要三步:
1.把n-1个盘子借助c柱子从a柱子移动到b柱子
2.把剩下的那个最大的盘子从a柱子移动到c柱子
3.把n-1个盘子借助a柱子从b柱子移动到c柱子
def hanoi(n, a, b, c):
if n == 1:
print(a, '-->', c)
else:
hanoi(n - 1, a, c, b)
print(a, '-->', c)
hanoi(n - 1, b, a, c)
# 调用
hanoi(5, 'A', 'B', 'C')
4.11 探索迷宫
# todo
4.12 动态规划
计算机科学中的许多程序是为了优化一些值而编写的; 例如,找到两个点之间的最短路径,找
到最适合一组点的线,或找到满足某些标准的最小对象集。计算机科学家使用许多策略来解
决这些问题。 动态规划 是这些类型的优化问题的一个策略。
4.13 总结
- 所有递归算法都必须具有基本情况。
- 递归算法必须改变其状态并朝基本情况发展。
- 递归算法必须调用自身(递归)。
- 递归在某些情况下可以代替迭代。
- 递归算法通常可以自然地映射到你尝试解决的问题的表达式。
- 递归并不总是答案。有时,递归解决方案可能比迭代算法在计算上更昂贵。