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()