目录
一、什么是递归Recursion
递归是一种解决问题的方法,它将问题分解为规模更小的相同问题,在算法流程中调用自身
1. 初识递归
1.1 数列求和
问题:
给定一个不定长列表, 返回所有数的和。需要一个循环和一个累加变量来迭代求和。
举个例子:
求全括号表达式的和:
t
o
t
a
l
=
(
1
+
(
3
+
(
5
+
(
7
+
9
)
)
)
)
total =(1+(3+(5+(7+9))))
total=(1+(3+(5+(7+9))))
最内层的括号(7+9), 这是无需循环即可计算的, 实际上整个求和的过程是这样:
t
o
t
a
l
=
(
1
+
(
3
+
(
5
+
16
)
)
)
total = (1+(3+(5+16)))
total=(1+(3+(5+16)))
t
o
t
a
l
=
(
1
+
(
3
+
21
)
)
total = (1+(3+21))
total=(1+(3+21))
t
o
t
a
l
=
(
1
+
24
)
total = (1+24)
total=(1+24)
t
o
t
a
l
=
25
total =25
total=25
- 观察上述过程中所包含的重复模式, 可以把求和问题归纳成这样:
数 列 的 和 = “ 首 个 数 ” + “ 余 下 数 列 ” 的 和 数列的和=“首个数”+“余下数列”的和 数列的和=“首个数”+“余下数列”的和 - 如果数列包含的数少到只有1个的话, 它的和就是这个数了-- 这是规模小到可以做最简单的处理
程序如下:
def listsum(numList):
if len(numList) == 1:
return numList[0]
else:
return numList[0] + listsum(numList[1:])
print(listsum([1,3,4,2,7]))
1.2 递归“三定律”
- 递归算法必须有一个基本结束条件(最小规模问题的直接解决)
- 递归算法必须能改变状态向基本结束条件演进(减小问题规模)
- 递归算法必须调用自身(解决减小了规模的相同问题)
2. 递归的应用
2.1 任意进制转换
- 我们用最熟悉的十进制分析下这个问题
- 我们用整数除, 和求余数两个计算来将整数一步步拆开
– 除以“进制基base”(// base)
– 对“进制基”求余数(% base) - 问题就分解为:
– 余数总小于“进制基base”,是“基本结束条件”,可直接进行查表转换
– 整数商成为“更小规模”问题,通过递归调用自身解决
代码如下:
def toStr(n,base):
convertString = "0123456789ABCDEF"
if n<base:
return convertString[n]
else:
return toStr(n//base,base) + convertString[n%base]
print (toStr(1453,16))
Python中的递归深度限制
- 在Python内置的sys模块可以获取和调整最大递归深度
import sys
print(sys.getrecursionlimit()) #1000
sys.setrecursionlimit(3000)
print(sys.getrecursionlimit()) #3000
推荐两部关于闭环递归的电影: 《predestination》–前目的地,《Triangle》–恐怖邮轮
2.2 递归可视化:分形树
下面我们通过递归作图来展现递归调用的视觉影像。
2.2.1 Python的海龟作图系统turtle module
-
Python内置,随时可用,以LOGO语言的创意为基础
-
其意象为模拟海龟在沙滩上爬行而留下的足迹
– 爬行: forward(n); backward(n)
– 转向: left(a); right(a)
– 抬笔放笔: penup(); pendown()
– 笔属性: pensize(s); pencolor©
PS:用这个画图,系统容易崩 -
画一个五角星:
import turtle
t = turtle.Turtle()
t.pencolor('red')
t.pensize(3)
for i in range(5):
t.forward(100)
t.right(144)
t.hideturtle() #隐藏画笔的turtle形状
turtle.done()
- 一个递归作图的例子:螺旋
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()
2.3 找零兑换问题的递归解法
问题: 假设你为一家自动售货机厂家编程序,自动售货机要每次找给顾客最少数量硬币;
解决方案:
-
肯定能找到最优解的方法 – 贪心策略
(1). 确定基本结束条件
需要兑换的找零,其面值正好等于某种硬币
(2). 减少问题的规模,对每种硬币尝试1次, 例如美元硬币体系:找零减去1分(penny)后,求兑换硬币最少数量(递归调用自身); 找零减去5分(nikel)后,求兑换硬币最少数量 找零减去10分(dime)后,求兑换硬币最少数量 找零减去25分(quarter)后,求兑换硬币最少数量 上述4项中选择最小的一个。
n u m C o i n s = m i n { 1 + n u m C o i n s ( o r i g i n a l a m o u t − 1 ) 1 + n u m C o i n s ( o r i g i n a l a m o u t − 5 ) 1 + n u m C o i n s ( o r i g i n a l a m o u t − 10 ) 1 + n u m C o i n s ( o r i g i n a l a m o u t − 25 ) numCoins=min\left\{ \begin{matrix} \begin{matrix} \begin{matrix} 1+numCoins(originalamout-1) \\ 1+numCoins(originalamout-5) \\ \end{matrix} \\ 1+numCoins(originalamout-10) \\ \end{matrix} \\ 1+numCoins(originalamout-25) \\ \end{matrix} \right. numCoins=min⎩⎪⎪⎨⎪⎪⎧1+numCoins(originalamout−1)1+numCoins(originalamout−5)1+numCoins(originalamout−10)1+numCoins(originalamout−25)
[注]:1表示其面值正好等于某种硬币,因此只需要1个硬币(3). 递归解法代码:
## 虽然能解决问题,但极其低效
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 + recMC(coinValueList, change-i) #调用自身:每次减去一种硬币面值挑选最小数量
if numCoins < minCoins:
minCoins = numCoins
return minCoins
print (recMC([1, 5, 10, 25], 63))
- 递归算法改进
(1). 消除重复计算
可以用一个表将计算结果保存起来
(2). 算法的中间结果是部分找零的最优解
在递归调用之前,先查找表中是否有部分找零的最优解
如果有,直接返回最优解
如果没有,进行递归调用
(3). 改进算法代码:
def recDC(coinValueList, change, knownResults):
minCoins = change
if change in coinValueList:
knownResults[change] = 1
return 1 #最小规模,直接返回
elif knownResults[change] > 0:
return knownResults[change] #查表成功,直接用最优解
else:
for i in [c for c in coinValueList if c <= change]:
#调用自身:每次减去一种硬币面值挑选最小数量
numCoins = 1 + recDC(coinValueList, change-i, knownResults)
if numCoins < minCoins:
minCoins = numCoins
knownResults[change] = minCoins #找到最优解,记录在表中
return minCoins
print (recDC([1, 5, 10, 25], 63, [0]*64))
- 动态规划解法 – 非递归函数
(1). 动态规划算法采用了一种更有条理的方式来得到问题的解。
(2). 从最简单的“一分钱找零”的最优解开始,逐步递加上去,知道我们需要的找零钱数。
(3). 问题的最优解包含了更小规模问题的最优解,这是一个最优化问题能够用动态规划策略解决的必要条件。
(4) 动态规划算法代码
def dpMakeChange(coinValueList, change, minCoins):
for cents in range(1, change+1): #从1开始到change逐个计算最少硬币书
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]
print(dpMakeChange([1, 5, 10, 21, 25], 63, [0]*64))
- 动态规划算法扩展
在生成最优解列表的同时,跟踪记录所选择的那个硬币币值,从而得到最优解硬币组合
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 #记录本步骤加的1个硬币
return minCoins[change]
def printCoins(coinsUsed, change):
coin = change
while coin > 0:
thisCoin = coinsUsed[coin]
print(thisCoin)
coin = coin - thisCoin
amnt = 63
clist = [1, 5, 10, 21, 25]
coinsUsed = [0] * (amnt + 1)
coinsCount= [0] * (amnt + 1)
print("Making change for ", amnt, "requires")
print(dpMakeChange(clist, amnt, coinsCount,coinsUsed), "coins")
print("They are:")
printCoins(coinsUsed, amnt)
print("Th used list is as follows:")
print(coinsUsed)