遞歸(Recursion)及其應用

1. 什麼是遞歸

遞歸是一種解決問題的方法,其精髓在於將問題分解爲規模更小的相同問題,持續分解,直到問題規模小到可以用非常簡單直接的方式來解決。
遞歸問題分解方式非常獨特,其算法方面的明顯特徵就是:在算法流程中調用自身。

2. 遞歸的應用

2.1 數列求和

數列求和[1,3,5,7,9]
換個方式來表達數列求和:全括號表達式(1+(3+(5+(7+(9)))))
上面這個式子,最內層的括號(7+9)是無需循環即可計算的,實際上整個求和過程如下:
在這裏插入圖片描述
觀察上述過程中所包含的重複模式,可以把求和問題歸納成這樣:
數列的和=“首個數”+“餘下數列”
如果數列包含的數燒到只有1個的話,它的和就是這個數。
在這裏插入圖片描述

上述遞歸算法的python實現

def listsum(numList):
    if len(numList)==1:
        return numList[0]
    else:
        return numList[0]+listsum(numList[1:])
print(listsum([1,3,5,7,9]))

上面程序的要點:
1.問題分解爲更小規模的相同問題,表現爲“調用自身
2.對最小規模問題的解決:簡單直接
遞歸函數調用和返回過程的鏈條如下圖所示
在這裏插入圖片描述

遞歸“三定律”

爲了向阿西莫夫的“機器人三定律”致敬,遞歸算法也總結出“三定律”
1.遞歸算法必須有一個基本結束條件(最小規模問題的直接解決)
2.遞歸算法必須能改變狀態向基本結束條件演進(減小問題規模)
3.遞歸算法必須調用自身(解決減小了規模的相同問題)
數列求和問題中的遞歸“三定律”
1.數列求和問題首先具備了基本結束條件:當列表長度爲1的時候,直接輸出所包含的唯一數
2.數列求和處理的數據對象是一個列表,而基本結束條件是長度爲1的列表,那遞歸算法就要改變列表並向長度爲1的狀態演進
3.調用自身。更短數列的求和問題。

2.2 整數轉換爲任意進制

餘數總小於“進制基base”是“基本結束條件”,可直接進行查錶轉換
整數商成爲“更小規模”問題,通過遞歸調用自身解決。
在這裏插入圖片描述
####python代碼實現

#整數轉任意進制
def toStr(n,base):
    convertString="0123456789ABCDEF"
    if n <base:
        return convertString[n]
    else:
        return toStr(n//base,base)+convertString[n%base]
    
print(toStr(34,2))

2.3 漢諾塔

漢諾塔問題是法國數學家Edouard Lucas於1883年根據傳說提出來的。
傳說在一個印度教寺廟裏,有3根柱子,其中一根套着64個由小到大的黃金盤片,僧侶們的任務就是要把這一疊黃金盤片從一個柱子搬到另一根,但有兩個規則:
1.一次只能搬一個盤子;
2.大盤子不能疊在小盤子上
在這裏插入圖片描述

漢諾塔問題:遞歸思路

將盤片塔從開始柱,經由中間柱,移動到目標柱
首先將上層N-1個盤片的盤片塔從開始柱經由目標柱移動到中間柱
然後將第N個(最大的)盤片從開始柱移動到目標柱
最後將放置在中間柱的N-1個盤片的盤片塔經由開始柱移動到目標柱
基本結束條件,即最小規模問題是:1個盤片的移動問題

python代碼實現


#漢諾塔問題
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(3,"#1","#2","#3")

在這裏插入圖片描述

2.4 找零兌換

假設你爲一家自動售貨機廠家編程序,自動售貨機要每次找給顧客最少數量的硬幣
首先是確定基本結束條件,兌換硬幣找給問題最簡單直接的情況就是,需要兌換的找零其面值正好等於某種硬幣。如找零25分,答案就是1個硬幣!
其次是減小問題規模,對每種硬幣嘗試一次,例如美元硬幣體系:
找零減去1分(penny)後,求兌換硬幣最少數量(遞歸調用自身);
找零減去5分(nikel)後,求兌換硬幣最少數量
找零減去10分(dime)後,求兌換硬幣最少數量
找零減去25分(quarter)後,求兌換硬幣最少數量。
上述四項中選擇最小的一個。
在這裏插入圖片描述

python代碼實現

          
#找零問題
import time
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(time.clock())
print(recMC([1,5,10,25],63))
print(time.clock())

遞歸解法雖然能解決問題,但其最大的問題是非常低效。
對63分的兌換硬幣問題,需要進行67,716,925次遞歸調用!在我的筆記本電腦上花費了近50秒的時間得到解:6個硬幣。
在這裏插入圖片描述
對這個遞歸解法ji9nxing改進的關鍵在於消除重複計算,我們可以用一個表將計算過的中間結果保存起來,在計算之前查表看看是否已經計算過。
這個算法的中間結果就是部分找零的最優解,在遞歸調用過程中已經得到的最優解被記錄下來。在遞歸調用之前,先查找表中是否已有部分找零的最優解。如果有,直接返回最優解而不進行遞歸調用;如果沒有,才進行遞歸調用。

python代碼實現

#找零問題改進算法
import time 
def recDC(coinValueList,change,knowResults):
    minCoins=change
    if change in coinValueList:#遞歸基本結束條件
        knowResults[change]=1#記錄最優解
        return 1
    elif knowResults[change]>0:
        return knowResults[change]#查表成功,直接用最優解
    else:
        for i in [c for c in coinValueList if c< change]:
            numCoins=1+recDC(coinValueList,change-i,knowResults)
            if numCoins<minCoins:
               minCoins=numCoins
               #找到最優解記錄到表中
               knowResults[change]=minCoins
    return minCoins
memo=[0]*64
print(time.clock())
print(recDC([1,5,10,25],63,memo))
print(time.clock())
print(memo)

改進後的解法,極大地減少了遞歸調用次數,對63分兌換硬幣問題,僅需要221次遞歸調用,是改進前的三十萬分之一,瞬間返回!
在這裏插入圖片描述

3.遞歸可視化:圖示

3.1 python的海歸作圖系統turtle module

python內置,隨時可用,以LOGO語言的創意爲基礎,其意象爲模擬海龜在沙灘爬行而留下的足跡。
爬行:forward(n);backward(n)
轉向:left(a);right(a)
擡筆放筆:penup();pendown()
筆屬性:pensize(s);pencolor©

3.1.1 長度爲100的直線

import turtle
t=turtle.Turtle()
#開始作圖
t.forward(100)#指揮海龜作圖
#作圖結束
turtle.done()

在這裏插入圖片描述

3.1.2 正方形

#畫一個正方形
import turtle
t=turtle.Turtle()
#開始作圖
for i in range(4):
    t.forward(100)
    t.right(90)
turtle.done()

在這裏插入圖片描述

3.1.3 五角星

#畫一個五角星
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.done()

在這裏插入圖片描述

3.1.4 螺旋

#螺旋
import turtle
t=turtle.Turtle()
def drawSpiral(t,linelen):
    if linelen>0:#最小規模,0直接退出
        t.forward(linelen)
        t.right(90)
        drawSpiral(t,linelen-5)#調用自身,減小規模,邊長減小5
drawSpiral(t,100)
turtle.done()

在這裏插入圖片描述

3.2 分形數:自相似遞歸圖形

分形Fractal,是1975年由Mandelbrot開創的新學科。“一個粗糙或零碎的幾何形狀,可以分成數個部分,且每一部分都(至少近似地)是整體縮小後的形狀”,即具有自相似的性質。
分形是在不同尺度上都具有相似性的事物,我們能看出一棵樹的每個分叉和每條樹枝,實際上都具有整棵樹的外形特徵(也是逐步分叉的)。這樣,我們可以把樹分解爲三個部分:樹幹、左邊的小樹、右邊的小樹。分解後,正好符合詆譭的定義:對自身的調用
在這裏插入圖片描述

python代碼實現

import turtle
t=turtle.Turtle()
t.pencolor('green')
t.pensize(3)
def tree(branch_len):
    if branch_len>5:#樹幹太短不畫,即遞歸結束條件
        t.forward(branch_len)
        t.right(20)#右傾斜20度
        tree(branch_len-10)#遞歸調用,畫右邊的小樹,樹幹減10
        t.left(40)#向左回40度,即左傾斜20度
        tree(branch_len-10)#遞歸調用,畫左邊的小樹,樹幹減10
        t.right(20)#向右回20度,即回正
        t.backward(branch_len)#海歸退回原位置
        
t.left(90)
t.penup()
t.backward(100)
t.pendown()
tree(75)#畫樹幹長度爲75的二叉樹
t.hideturtle()
turtle.done()

在這裏插入圖片描述

3.3 遞歸可視化:謝爾賓斯基三角形

根據自相似特性,謝爾賓斯基三角形是由3個尺寸減半的謝爾賓斯基三角形按照品字形拼疊而成。由於我們無法真正作出謝爾賓斯基三角形(degree趨於無窮),只能做degree有限的近似圖形。
在degree有限的情況下,degree=n的三角形是由3個degree=n-1的三角形按照品字形拼疊而成。同時,這3個degree=n-1的三角形邊長均爲degree=n的三角形的一半(規模減小)。當degree=0,則就是一個等邊三角形,這是遞歸基本結束條件。
在這裏插入圖片描述

python代碼實現

#遞歸可視化:謝爾賓斯基三角形
import turtle
def sierpinski(degree,points):
    colormap=['blue','red','green','white','yellow','orange']
    drawTriangle(points,colormap[degree])#畫等邊三角形
    if degree>0:#最小規模,0直接退出
        #減小規模,getMid邊長減半,調用自身,左上右次序
        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(points['top'])
    t.pendown()
    t.begin_fill()
    t.goto(points['left'])
    t.goto(points['right'])
    t.goto(points['top'])
 
#取兩個點的中點
def getMid(p1,p2):
    return ((p1[0]+p2[0])/2,(p1[1]+p2[1])/2)
    
t=turtle.Turtle()
t.pensize(2)
points={'left':(-200,-100),'top':(0,200),'right':(200,-100)}
#畫degree=5的三角形
sierpinski(5,points)
turtle.done()

在這裏插入圖片描述

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