数据结构与算法(python)递归:找零问题

参考自 MOOC数据结构与算法Python版

一、什么是递归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 递归“三定律”
  1. 递归算法必须有一个基本结束条件(最小规模问题的直接解决)
  2. 递归算法必须能改变状态向基本结束条件演进(减小问题规模)
  3. 递归算法必须调用自身(解决减小了规模的相同问题)

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. 肯定能找到最优解的方法 – 贪心策略
    (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=min1+numCoins(originalamout1)1+numCoins(originalamout5)1+numCoins(originalamout10)1+numCoins(originalamout25)
    [注]: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. 递归算法改进
    (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. 动态规划解法 – 非递归函数
    (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))
  1. 动态规划算法扩展
    在生成最优解列表的同时,跟踪记录所选择的那个硬币币值,从而得到最优解硬币组合
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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值