數據結構與算法:經典遞歸算法與動態規劃算法的Python實現

遞歸-將大規模問題分解爲小規模問題,在算法流程中調用自身

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學習者聚集地

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章