1、递归Recursion
把某些问题分解为规模更小的相同问题,特点是在算法流程中调用自身
(1)数列求和(给定一个列表,返回所有数的和)
即不能用for也不能用while.数列求和可以分解为两个数的求和。用全括号表达式表示求和
(1+(3+(5+(7+9))))
故数列的和=第一个数的和+余下数列的和,
只有一个数的时候就结束了
listSum(nunmlist)=first(numlist)+ listSum(rest(numlist))
def listsum(numlist):
if len(numlist)==1:
return unmlist[0]
else:
return numlist[0]+listsum(numlist[1:])
要点:
问题分解为最小的规模并表现为调用自身
对最小规模问题的解决简单直接
(2)递归算法三定律
(a)必须有一个基本结束条件(最小规模问题的直接解决)
(b)递归算法必须能改变状态向基本结束条件演进(要能减小问题规模)
©必须调用自身去解决减小了的问题
2、递归的应用:任意进制转换
整数商可以化为规模更小的问题,余数小于进制基,可以直接转化。
停止则是该数小于进制基base
def toStr(n,base):
convertString = "0123456789ABCDEF"
if n < base:
return convertString[n]
else:
return toStr(n//base,base)+convertString[n%base]
3、递归调用的实现
当一个函数被调用的时候,系统会把调用时的现场数据压入到系统调用栈。每次调用,压入栈的现场数据称为栈帧。当函数返回时,要从调用栈的栈顶取得返回地址,恢复现场,弹出栈帧,按地址返回。本质就是先入后出的栈方法
从上面可以看到,递归调用的实现依赖于栈结构,因此如果没有结束条件或者向基本结束条件演进太慢,导致递归层数太多,调用栈溢出
在python内置的sys模块可以获取和调整最大递归深度
import sys
sys.setrecursionlimit(3000)
print(sys.getrecursionlimit())
结果为3000,改变之前一般是1000
4、递归可视化:分形树
用海龟作图模式画图
画螺旋线
import turtle
t=turtle.Turtle()
def drawSpiral(t,lineLen):
if lineLen>0:
t.forward(lineLen)
t.right(90)
drawSpiral(t,lineLen-5)
drawSpiral(t,100)
turtle.done()
二叉树=树干+倾斜的右小树+倾斜的左小树
分形树代码:
import turtle
def tree(branch_len):
if branch_len>5:
t.forward(branch_len)
t.right(20)
tree(branch_len-15)
t.left(20)
tree(branch_len-15)
t=turtle.Turtle()
t.left(90)#画笔初始位于底部朝右
t.penup()
t.forward(100)
t.pendown()#调整画笔位置
t.pencolor('green')
t.pensize(2)
tree(75)
t.hideturtle()
turtle.done()
5、递归可视化:谢尔宾斯基三角形
构造方法:
(1).取一个实心的三角形。(多数使用等边三角形)
(2)沿三边中点的连线,将它分成四个小三角形。
(3)去掉中间的那一个小三角形。
(4)对其余三个小三角形重复1。
取一个正方形或其他形状开始,用类似的方法构作,形状也会和谢尔宾斯基三角形相近。
其面积为0当周长无穷,是介于二维和一维之间的分数维。
第n个三角形由三个n-1三角形组成,它们的边长是n的一半,0时是一个等边三角形
import turtle
def sirepinski(degree,points):#points是字典,表示点坐标
#用left,top和right表示
colormap=['blue','red','green','white','yellow','orange']
drawTriangle(points,colormap[degree])
if degree>0:
sierpinski(degree-1,{'left':points['left']},'top':getMid(points['left'],points['top']),'right':getMid(points['left'],points['right']))
sierpinski(degree-1,{'left':getMid(points['left'],points['top']),'top':points['top'],'right':getMid(points['top'],points['right']))
sierpinski(degree-1,{'left':getMid(points['left'],points['right']),'top':getMid(points['top'],points['right']),'right':points['right'])
def drawTriangle(points,color):
t.fillcolor(color)
t.penup()
t.goto(point['top'])
t.pendown()
t.begin_fill()
t.goto(point['left'])
t.goto(point['right'])
t.goto(point['top'])
t.end_fill()
def getMid(p1,p2):
return( (p1[0]+p2[0])/2,(p1[1]+p2[1])/2)
t=turtle.Turtle()
points={'left':(-200,-100),'top':(0,200),'right':(200,-100)}
sierpinski(5,points)
turtle.done()
6、递归的应用:汉诺塔
三个柱子,其中一根套着64个从小到大盘子,一次只能搬一个盘子,大盘子不能叠到小盘子上。问能不能完成盘子的迁移
分解为n-1的问题,直到只剩一个盘子挪动的问题。
将三个柱子叫做开始柱,中间柱,目标柱
首先将N-1个盘片的盘片塔从开始柱经由目标柱,移动到中间柱,然后将第n个盘片从开始柱移动到目标柱。最后将放置在中间柱的N-1个盘片经由开始柱,移动到目标柱。
结束条件:一个盘片的问题
def movetower(height,fromPole,withPole,toPole):
if height >=1:
movetower(height-1,fromPole,toPole,withPole)
moveDisk(height,fromPole,toPole)
moveTower(height-1,withPole,fromPole,toPole)
def moveDisk(disk,fromPole,toPole):
print(f"Moving disk[{disk}] from{fromPole}to {toPole}")
moveTower(2,"#1","#2","#3")
7、递归的应用:探索迷宫
将海龟放在迷宫中间,找出口
首先我们将整个迷宫的空间(矩形)分为行列整齐的方格,区分出墙壁和通道。给每个方格具有行列位置,并赋予墙壁或通道的属性。
采用矩阵来实现迷宫数据结构,墙壁位置放’+’,海归投放点放’s’,从一个文本文件逐行读入迷宫数据。
class Maze:
def __init__(self,mazeFilename):
rowsInMaze=0
columnsInMaze=0
self.mazelist=[]
mazeFile=open(mazeFileName,'r')
rowsInMaze=0
for line in mazeFile:
rowlist=[]
col=0
for ch in line[:-1]:
rowList.append(ch)
if ch=='S':
self.startRow=rowsInmate
self.startCol=col
col=col+1
rowsInMaze+=1
self.mazelist.append(rowList)
columnsInMaze=len(rowList)
海龟移动方向是前后左右,如果某个方向是墙壁,就要换个方向移动。
将海归从原位置向北移动一步,以新位置递归调用探索迷宫寻找出口。
如果上述步骤找不到出口,那么从原位置向南,以新位置递归调用
如果上述步骤找不到出口,那么从原位置向西,以新位置递归调用
如果上述步骤找不到出口,那么从原位置向东,以新位置递归调用。
如果都不行那就是没有出口
上述思路容易陷入无限递归的死循环,故要有一个机制记录海归走过的路线,把经过的路线做标记,不去已经去过的地方。在递归时立刻返回上一级。
故结束条件如下:
海归碰到墙壁,递归调用结束,返回失败
碰到已经走过的方格,递归调用结束,返回失败
碰到出口方格,即位于边缘的通道方格,递归调用结束,返回成功
在四个方向上探索都失败,递归调用结束,返回失败
import turtle
# 迷宫类
class Maze(object):
# 读取迷宫数据,初始化迷宫内部,并找到海龟初始位置。
def __init__(self, mazeFileName):
rowsInMaze = 0
columnsInMaze = 0
self.mazelist = []
mazeFile = open(mazeFileName, 'r')
for line in mazeFile: #按行读取
rowList = []
col = 0
for ch in line:
rowList.append(ch) #添加到行列表
if ch == 'S': #S为乌龟初始位置,即迷宫起点
self.startRow = rowsInMaze
self.startCol = col
col = col + 1
rowsInMaze += rowsInMaze
self.mazelist.append(rowList)
columnsInMaze = len(rowList) #获取迷宫总列数
self.rowsInMaze = rowsInMaze #设置迷宫总行数
self.columnsInMaze = columnsInMaze #设置迷宫总列数
self.xTranslate = -columnsInMaze/2 #设置迷宫左上角的初始x坐标
self.yTranslate = rowsInMaze/2 #设置迷宫左上角的初始y坐标
self.t = turtle.Turtle() #创建一个海龟对象
self.t.shape('turtle') #给当前指示点设置样式(类似鼠标箭头),海龟形状为参数指定的形状名,指定的形状名应存在于TurtleScreen的shape字典中。多边形的形状初始时有以下几种:"arrow", "turtle", "circle", "square", "triangle", "classic"。
self.wn = turtle.Screen() #创建一个能在里面作图的窗口
self.wn.setworldcoordinates(-columnsInMaze/2, -rowsInMaze/2, columnsInMaze/2, rowsInMaze/2) #设置世界坐标系,原点在迷宫正中心。参数依次为画布左下角x轴坐标、左下角y轴坐标、右上角x轴坐标、右上角y轴坐标
# 在屏幕上绘制迷宫
def drawMaze(self):
self.t.speed(20) #绘图速度
for y in range(self.rowsInMaze): #按单元格依次循环迷宫
for x in range(self.columnsInMaze):
if self.mazelist[y][x] == OBSTACLE: #如果迷宫列表的该位置为障碍物,则画方块
self.drawCenteredBox(x + self.xTranslate, -y + self.yTranslate, 'orange')
# 画方块
def drawCenteredBox(self, x, y, color):
self.t.up() #画笔抬起
self.t.goto(x - 0.5, y - 0.5) #前往参数位置,此处0.5偏移量的作用是使乌龟的探索路线在单元格的正中心位置
self.t.color(color) #方块边框为橙色
self.t.fillcolor('green') #方块内填充绿色
self.t.setheading(90) #设置海龟的朝向,标准模式:0 - 东,90 - 北,180 - 西,270 - 南。logo模式:0 - 北,90 - 东,180 - 南,270 - 西。
self.t.down() #画笔落下
self.t.begin_fill() #开始填充
for i in range(4): #画方块边框
self.t.forward(1) #前进1个单位
self.t.right(90) #右转90度
self.t.end_fill() #结束填充
# 移动海龟
def moveTurtle(self, x, y):
self.t.up() #画笔抬起
self.t.setheading(self.t.towards(x + self.xTranslate, -y + self.yTranslate)) #setheading()设置海龟朝向,towards()从海龟位置到由(x, y),矢量或另一海龟位置连线的夹角。此数值依赖于海龟初始朝向,由"standard"、"world"或"logo" 模式设置所决定。
self.t.goto(x + self.xTranslate, -y + self.yTranslate) #前往目标位置
# 画路径圆点
def dropBreadcrumb(self, color):
self.t.dot(color) #dot(size=None, color)画路径圆点
# 用以更新迷宫内的状态及在窗口中改变海龟位置,行列参数为乌龟的初始坐标。
def updatePosition(self, row, col, val):
self.mazelist[row][col] = val #设置该标记状态为当前单元格的值
self.moveTurtle(col, row) #移动海龟
if val == PART_OF_PATH: #其中一条成功路径的圆点的颜色
color = 'green'
elif val == TRIED: #尝试用的圆点的颜色
color = 'black'
elif val == DEAD_END: #死胡同用的圆点的颜色
color = 'red'
self.dropBreadcrumb(color) #画路径圆点并上色
# 用以判断当前位置是否为出口。
def isExit(self, row, col):
return (row == 0 or row == self.rowsInMaze - 1 or col == 0 or col == self.columnsInMaze - 1) #根据海龟位置是否在迷宫的4个边线位置判断
# 返回键对应的值,影响searchFrom()中maze[startRow][startColumn]值的获取
def __getitem__(self, key):
return self.mazelist[key]
# 探索迷宫,注意此函数包括三个参数:一个迷宫对象、起始行、起始列。
def searchFrom(maze, startRow, startColumn):
# 从初始位置开始尝试四个方向,直到找到出路。
# 1. 遇到障碍
if maze[startRow][startColumn] == OBSTACLE:
return False
# 2. 发现已经探索过的路径或死胡同
if maze[startRow][startColumn] == TRIED or maze[startRow][startColumn]== DEAD_END:
return False
# 3. 发现出口
if maze.isExit(startRow, startColumn):
maze.updatePosition(startRow, startColumn, PART_OF_PATH)#显示出口位置,注释则不显示此点
return True
maze.updatePosition(startRow, startColumn, TRIED)#更新迷宫状态、设置海龟初始位置并开始尝试
# 4. 依次尝试每个方向
found = searchFrom(maze, startRow - 1, startColumn) or \
searchFrom(maze, startRow + 1, startColumn) or \
searchFrom(maze, startRow, startColumn - 1) or \
searchFrom(maze, startRow, startColumn + 1)
if found: #找到出口
maze.updatePosition(startRow, startColumn, PART_OF_PATH)#返回其中一条正确路径
else: #4个方向均是死胡同
maze.updatePosition(startRow, startColumn, DEAD_END)
return found
if __name__ == '__main__':
PART_OF_PATH = 'O' #部分路径
TRIED = '.' #尝试
OBSTACLE = '+' #障碍
DEAD_END = '-' #死胡同
myMaze = Maze('maze.txt') #实例化迷宫类,maze文件是使用“+”字符作为墙壁围出空心正方形空间,并用字母“S”来表示起始位置的迷宫文本文件。
myMaze.drawMaze() #在屏幕上绘制迷宫。
searchFrom(myMaze, myMaze.startRow, myMaze.startCol) #探索迷宫
8、分治策略
将问题分为若干的更小的问题,通过解决每一个小规模的部分问题,并将结果汇总(merge)得到原问题的解。
递归也体现了分治策略。
9、优化问题和贪心算法
找给顾客数量最少的硬币(25,10,1)
贪心策略是最直观的方法,先从面值最大的硬币开始,用尽量多的数量,再用下一个面值最大的硬币,直到找完零。但只能求出局部最优解,在一小部分情况下才能找到最优结果
10、找零问题的递归解法
首先确定基本结束条件,即需要兑换的找零恰好等于某种硬币的面值。
其次是减小问题规模,要对每一种硬币尝试一次,例如找零减去1/5/10/25分后进行调用,求最少的兑换数量,再选最少的一个就一定是最优解,这是因为不论何种找零情况都肯定用过1,5,10,25之中的一个,不妨设为1,则去掉1后的硬币数不小于情况一,故上述四种情况中存在最优解。
代码如下:
def recMC(coinValueList,change):
minCoins=change
if change in coinValueList:
return 1
else:
for i in [c for c in coinValueList if c<=change]:#去掉面值大于找零数额的,否则程序无法停止
numCoins = 1+recMCcoinValueList,change-i)
if minCoins>numCoins:
minCoins=numCoins
return minCoins
此种方法及其低效,重复计算太多(找零26块钱时有可能多次计算15块钱的最优解)
改进方法:保存中间结果
def recMC(coinValueList,change,knownResults):
minCoins=change
if change in coinValueList:
knownResults[change]=1#记录最优解
return 1
elif change in knownResults.keys():
return knownResults[change]#查表成功
else:
for i in [c for c in coinValueList if c<=change]:#去掉面值大于找零数额的,否则程序无法停止
numCoins = 1+recMC(coinValueList,change-i,knownResults)
if minCoins>numCoins:
minCoins=numCoins
knownResults[change]=minCoins
return minCoins
print(recMC([1,5,10,25],63,{}))
11、找零问题的动态规划解法
动态规划算法是用一种更有条理的方式来得到问题的解。从最简单的1分钱找零开始逐步加上去,直到找到我们需要的零钱数。
故使用动态规划的前提条件是大问题的最优解是由更小问题的最优解组成
代码如下:
def dpMakeChange(coinValueList,change,minCoins):
for cents in range(1,change+1):
coinCount=cents
for j in [c for c in coinValueList if c<=cents]:
if minCoins[cents-j]+1<coinCount:
coinCount=minCoins[cents-j]+1
minCoins[cents]=coinCount
return minCoins[change]
扩展:最少币值组合
只需要在生成最优解列表时在生成一个列表来记录寻找当前零钱数时所选择的那个硬币的币值即可。在得到最后的解后,减去选择的硬币币值,回溯到表格之前的部分找零,就能逐步得到每一步所选择的硬币币值。
def dpMakeChange(coinValueList,change,minCoins,coinsUsed):
for cents in range(1,change+1):
coinCount=cents
newCoin=1#初始化新加入的硬币
for j in [c for c in coinValueList if c<=cents]:
if minCoins[cents-j]+1<coinCount:
coinCount=minCoins[cents-j]+1
newCoin=j
minCoins[cents]=coinCount
coinsUsed[cents]=newCoin
return minCoins[change]
def printCoins(coinsUsed,change):
coin=change
while coin >0:
thisCoin=coinUsed[coin]
print(thisCoin)
coin-=thisCoin
12、动态规划案例分析
博物馆达到问题:5个物品,每个物品有重量和宝物,有重量限制,选总价值更高的组合。
把m(i,W)记为前i个宝物中组合不超过W重量,得到最大的价值。则m(i,W)应该是m(i-1,W)和m(i-1,W-Wi)+vi(按照第i件能否加入到W中来区分)两者的最大值。故应该从m(1,1)开始计算到m(5,20)。
#宝物的重量和价值
tr=[None,{'w':2,'v':3},{'w':3,'v':4},{'w':4,'v':8},{'w':5,'v':8},{'w':9,'v':10}]
#0号没有价值设为None
max_w=20#最大承重
#初始化二维表格m[(i,w)]
#表示前i个宝物中,最大重量w的组合所得到的最大价值
#当i什么都不取或w上限为0,价值均为0
m={(i,w):0 for i in range(len(tr))
for w in range(max_w+1)}
#逐步填写二维表格
for i in range(1,len(tr)):
for w in range(1,max_w+1):
if tr[i]['w']>w:#装不下第i个宝物
m[(i,w)]=m[(i-1,w)]#不装第i个宝物
else:#取最大
m[(i,w)]=max(
m[(i-1,w)],
m[(i-1,w-tr[i]['w'])]+tr[i]['v'])
print(m[(len(tr)-1,max_w)])
递归法
tr={(2,3),(3,4),(4,8),(5,8),(9,10)}
max_w=20
m={}
def thief(tr,w):
if tr==set() or w==0:
m[(tuple(tr),w)]#tuple是key的要求,变成元组
elif (tuple(tr),w) in m:
return m[(turple(tr),w)]
else:
vmax=0
for t in tr:#每次挑一个去掉
if t[0]<=w:
v=thief(tr-{t},w-t[0])+t[1]
vmax=max(vmax,v)
m[(tuple(tr),w)]=vmax
return vmax