数据结构与算法:经典递归算法与动态规划算法的Python实现

本文深入探讨了递归算法和动态规划在解决问题中的应用,通过Python实现展示了从基本的进制转换、分形绘制到汉诺塔、迷宫探索,再到找零和01背包问题的解决方案。递归遵循三大定律,动态规划则通过保存子问题的最优解避免重复计算,两者在优化问题上展现出不同的优势。
摘要由CSDN通过智能技术生成

递归-将大规模问题分解为小规模问题,在算法流程中调用自身

def listsum(numlist):
    if len(numlist)==1:
        return numlist[0]
    else:
        return numlist[0]+listsum(numlist[1:])

print(listsum([1,2,3,4]))

基本结束条件必须有,递归要能减少问题规模,调用自身-递归三定律

1 任意进制转换
基本结束条件:小于n进制的整数,进制基求余数,整数除大化小

def toStr(n,base):
    convertString="0123456789ABCDEF"
    if n<base:
        return convertString[n]
    else:
        return toStr(n//base,base)+convertString[n%base]

print(toStr(6,2))

递归调用的实现:当一个函数被调用的时候,系统会把调用时的现场数据压入到系统调用栈
压入栈的【现场数据】称为【栈帧】。当函数返回时,从栈顶取得返回地址,恢复现场,弹出
栈帧,按地址返回。调用顺序和返回顺序相反。
Recursion Error:可能是忘设结束条件,或者溢出系统调用栈容量

import sys
print(sys.getrecursionlimit())
sys.setrecursionlimit(2000)
sys.getrecursionlimit()

#|递归可视化,分形树
#turtle模块,海龟作图系统Python内置
import turtle
t=turtle.Turtle()
"""
#draw a five-angles star
t.pencolor('blue')
t.pensize(8)
for i in range(5):
    t.forward(100)
    t.right(144)
t.hideturtle()#hide the arow
turtle.done()

Fractal分形,几何形状的局部和整体自相似
自然现象中的分形特征-二叉树

2 绘制分形树

def tree(branch_len):
    if branch_len>1:
        t.forward(branch_len)
        t.right(20)
        tree(branch_len-10)
        t.left(40)
        tree(branch_len-10)
        t.right(20)
        t.backward(branch_len)

t=turtle.Turtle()
t.left(90)
t.penup()
t.backward(100)
t.pendown()
t.pencolor('grey')
t.pensize(4)
tree(100)
turtle.done()

Sierpinski Triangle谢尔宾斯基三角形
每一次迭代边长减半,degree=0的时候就是一个等边三角形

def sierpinski(degree,points):
    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,{'top':points['top'],
                    'left':getMid(points['left'],points['top']),
                    'right':getMid(points['top'],points['right'])})
        sierpinski(degree-1,{'right':points['right'],
                    'top':getMid(points['right'],points['top']),
                    'left':getMid(points['left'],points['right'])})        

def drawTriangle(points,color):
    t.fillcolor(color)
    t.penup()
    t.goto(points['top'])
    t.pendown()
    t.begin_fill()
    t.goto(points['left'])
    t.goto(points['right'])
    t.goto(points['top'])
    t.end_fill

def getMid(p1,p2):
    return ((p2[0]+p1[0])/2,(p2[1]+p1[1])/2)

points={'left':(-200,-100),'top':(0,200),'right':(200,-100)}
#sierpinski(5,points)
#turtle.done()

3 汉诺塔

N个盘子一堆要从1号柱移到3号柱,一次只能移动1层,且上层不能低于下层
将问题拆解成第n个盘子与前n-1个盘子的移动,实现问题的缩小和函数自调用

def mveTower(height,fromPole,withPole,toPole):
    if height>=1:
        mveTower(height-1,fromPole,toPole,withPole)
        moveDisk(height,fromPole,toPole)
        mveTower(height-1,withPole,fromPole,toPole)

def moveDisk(disk,fromPole,toPole):
    print(f"Moving disk[{disk}] from {fromPole} to {toPole}.")

mveTower(2,"#1","#2","#3")

4 海龟探索迷宫

迷宫数据结构,字符列表的列表,+墙壁, 通道,s起点
定义递归算法:海龟从原位置向北移动一步,以新位置递归调用探索迷宫寻找出口,如果这样找
不到出口,那么将海龟从原位置向南移动一步,以新位置递归调用探索迷宫
南也找不到,则回到原位置,向西移动一步,递归继续,同理向东。
无限递归问题:向北一步之后再向北是墙,则回到原点再向北,进入无限循环
面包屑机制:留下记录,一旦走回头路则换下一个方向调用递归。
结束条件:墙壁-返回失败,面包屑-返回失败,四个方向都失败-没有出路-失败,出口-成功
核心探索函数-“好文共欣赏,疑义相与析。”

def searchFrom(maze,startRow,StartColumn):
    maze.updatePosition(startRow,startColumn)
    if maze[startRow][StartColumn]== OBSTACLE:
        return False
    if maze[startRow][StartColumn]==TRIED or maze[startRow][StartColumn]==DEAD_END:
        return False
    #结束条件-出口-返回True
    if maze.isExit(startRow,StartColumn):
        maze.updatePosition(startRow,StartColumn,PART_OF_PATH)
        return True

    maze.updatePosition(startRow,StartColumn,TRIED)
    #短路机制,or会先检查前面的路径,Fasle之后才会尝试后面的Alternatives
    found=searchFrom(maze,searchFrom-1,StartColumn) or \
          searchFrom(maze,startRow+1,StartColumn) or \
          searchFrom(maze,startRow,StartColumn+1) or \
          searchFrom(maze,startRow,StartColumn-1)

    if found:#根据True返回值更新路径上节点的状态
        maze.updatePosition(startRow,StartColumn,PART_OF_PATH)
    else:
        maze.updatePosition(startRow,StartColumn,DEAD_END)
    return found#返回True/False,True则继续作路线图,False则根据短路代码调用其他方向的探索函数

5 分治策略

分而治之,将一个大问题分解成若干个更小规模的部分,并将结果汇总得到原问题的解
分治策略的应用相当广泛,排序、查找、遍历、求值

6 优化问题和贪心算法

优化问题示例: 找零个数最少化问题
贪心策略,最大化大面值代币数量,类推。
递归解法-一定能找到最优解的方法
确定基本结束条件:面值等于某种硬币
确定如何减少-减去各种面值,求兑换硬币最少数量-选择最小的一个

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

递归的问题是低效,需要重复计算
改进-消除重复计算,使用一个表将中间结果保存起来,在计算之前查表
这个算法的中间结果就是部分找零的最优解,如果计算过,则直接调用

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+recMC(coinValueList,change-i)#问题缩小
            if numCoins<minCoins:#求最小值
                minCoins=numCoins
                knownResults[change]=minCoins#每一级余额都存在这一级余额的最优解
    return minCoins
print(recDC([1,5,10,25],63,[0]*63))#运算量减少30万倍
#中间记录称作记忆化,函数值缓存,Memoization提高了递归解法的性能

7 找零问题的动态规划解法

动态规划算法更有条理,从1分钱找最优解开始,使得每一步都是最优解
依赖于更少钱数最优解的简单计算,这就有了【最优化问题能够用动态规划解决】的前提:问题最优解包含子问题最优解

#numCoins=min(1+numCoins(originalamount-coini))
def dpMakeChange(coinValueList,change,minCoins,coinsUsed):
    for cent in range(1,change+1):#输入金额格式转换为最小单位
        coinCounts=cent
        newCoin=1#记录值:初始化
        #从1分代币开始,逐个检查更多代币的最优组合
        for j in [c for c in coinValueList if c<= cent]:
            if minCoins[cent-j]+1<coinCounts:
                coinCounts=minCoins[cent-j]+1
                newCoin=j
        minCoins[cent]=coinCounts#建立一个最优解查询表
        coinsUsed[cents]=newCoin#记录对应某个特定余额,最后一个加入的币值
    return minCoins[change]
print(recDC([1,5,10,25],63,[0]*63))
#思想:从最简单开始寻找答案
#怎样记录所用硬币找零的值?coinsUsed[]
def printCoins(coinsUsed,change):
    coin=change
    while coin>0:
        thisCoin=coinsUsed[coin]
        print(thisCoin)
        coin-=thisCoin

8 01背包问题

解法1 动态规划

如何选择物品偷盗使得价值最高
m(i,w)前i个宝物中,重量不超过W,得到的最大价值
一定是max(m(i-1,w),vi+m(i-1),w-wi)
tr=[None,{‘w’:2,‘v’:3},{‘w’:3,‘v’:4},{‘w’:4,‘v’:8},{‘w’:5,‘v’:8},{‘w’:9,‘v’:10}]
max_w=20
缓存-前i个物品,最大重量w下的最大价值

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)必等于i-1的状态
            m[(i,w)]=m[(i-1,w)]
        else:#【核心】:dp状态转移公式,使用/不使用第i个物品,是决定前后最优状态的唯一因素
            m[(i,w)]=max(m[(i-1,w-tr[i]['w'])]+tr[i]['v'],m[(i-1,w)])
print(m[len(tr)-1,max_w])

解法2 递归

m={}#递归缓存字典
tr={(2,3),(3,4),(4,8),(5,8),(9,10)}
def thief(tr,w):
    #如果待选物品集合已经取空,或者最大限重为0,则m为0,此为结束条件
    if tr==set() or w==0:#set()创建无序不重复元素集合
        m[(tuple(tr),w)]=0#tuple()针对字典,返回key值元组
        return 0
    elif (tuple(tr),w) in m:#根据key值()查找最优解
        return m[tuple(tr),w]
    else:
        vmax=0
        for t in tr:
            if t[0]<= w:#要求待评价物品重量低于限重
                #求的是i种不同的i-1再加1个物品的方案,在当前w下的最大值
                v=thief(tr-{t},w-t[0])+t[1]#减少规模的递归调用自身,i-2问题,类推直到0结束
                vmax=max(vmax,v)
        m[(tuple(tr),w)]=vmax
        return vmax
print(thief(tr,max_w))

对比两种方法,计算都要从单取开始逐渐增多,动态规划的核心是状态转移公式,递归的核心是缩小问题范围,本质相通。
递归小结:

适用于自相似性问题,递归三定律:基本结构条件,减小问题规模,调用自身

递归算法能够于问题表达自然契合,但有时会引发巨量的重复计算,记忆化/函数值缓存可以有效减少重复计算

如果一个问题最优解包括规模更小相同问题的最优解,就可以用动态规划来解决

对Python感兴趣或者是正在学习的小伙伴,推荐我们的Python学习扣qun:784758214 ,看看前辈们是如何学习的!从基础的python脚本到web开发、爬虫、django、数据挖掘等【PDF,实战源码】,零基础到项目实战的资料都有整理。送给每一位python的小伙伴!每天都有大牛定时讲解Python技术,分享一些学习的方法和需要注意的小细节,点击加入我们的 python学习者聚集地

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值