以前公司管理系統項目須要配合前端VUE
實現動態路由,返回的數據結構是一個樹形結構,但在數據庫中存儲的是平行的數據項。這個時候就須要在代碼中去進行數據結構的組裝。很久沒思考這些問題,花了好些時間才搞定。這其中涉及到遞歸算法的實現,乘着週末深刻的思考了下遞歸算法的設計,一點點拙見寫下來作個筆記。前端
對於遞歸
其實咱們並不陌生。還記得中學時代常用的數學概括法
?,遞歸
其實並非計算機科學獨有的概念。實際上數學概括法
纔是遞歸的理論基礎。node
先舉個簡單的栗子:求n! 這裏使用遞歸來求解。算法
int foo(int n) {
if(n != 1) {
return n*foo(n-1);
}
return 1;
}
複製代碼
上面代碼所表達的含義是:當n
不等於1
的時候。一直去調用foo(n-1)
而且和傳入的n
相乘。在這裏解釋下遞歸調用的實現原理:咱們知道計算機中函數的調用是使用棧的數據結構實現的,好比傳入的n
爲5
時,第一次執行foo
方法,當執行到 foo(n-1)
,方法foo
會被壓入棧中。其實遞歸方法的出棧入棧和普通方法是同樣的。惟一的區別是遞歸調用的是自身的代碼。這裏不妨把foo
的每次調用稱做foo1
,foo2
等等。那麼這樣就好理解了,當foo
方法執行到n==1
的時候,棧裏面依次存放着foo1,foo2,foo3,foo4,foo5。
此時foo5
在棧頂,出棧執行。foo5
執行的時候n==1
,由代碼可知,n==1
,方法直接返回1
。接着foo4
出棧,傳入的n
爲2
,運算2*1
,而後foo3,foo2,foo1
依次出棧,運算依次爲:3*2,4*6
當最後一個方法foo1 return
時計算,5*24
,運行結束。數據庫
從這個過程當中不難看出,遞歸是個不斷降低的過程。層層調用自身,而後求值又是個上升的過程。從遞歸結束的出口返回值開始,層層向上求值。bash
從這個例子中還能夠看出遞歸算法的兩個重要特徵,好比:數據結構
須要思考的問題是終止條件是如何肯定的,以及他是怎麼變化的。在上面的程序中是作-1
操做。svg
和上面求階乘的例子類型同樣。斐波那契數列
的定義就是遞歸的。直接看代碼:函數
int Fib(int n) {
if(n==1 || n==2) {
return 1;
}else {
Fib(n-1)+Fib(n-2)
}
}
複製代碼
斐波那契數列
: 1,1,2,3,5 ...學習
最多見的好比鏈表的定義(這裏討論的是沒有頭節點的鏈表):ui
class Node {
private String data; //節點數據域
private Node next; //指向下一個節點
}
複製代碼
當須要對鏈表數據域求和時,可使用遞歸實現:
int SUM(Node node){
if(node == null) {
return 0;
}else {
return node.getData()+SUM(node.getNext());
}
}
複製代碼
這裏詳細討論下漢諾塔算法的實現。
漢諾塔問題描述:有三個分別叫作X,Y,Z的塔座。在塔座X上有直徑各不一樣,從小到大依次標號爲:1,2,3...n的盤片,如今要求把塔座X上的盤片移動到塔座Z上,按相同順序疊放。移動時須要遵照規則:1.每次只能移動一個盤片。 2.盤片能夠放在任意一個塔座上。 3.不能將較大的盤片放在較小的上面。
漢諾塔是典型的遞歸求解問題。光看描述不容易分析問題如何分解,不如在問題規模較小時,試着分析下解決方案。
當X塔座上只有一個盤片時:
X==>Z
當X塔座上有兩個盤片時:
X1==>Y,X2==>Z
Y1==>Z
當X塔座上有三個盤片時:
X1==>Z,X2==>Y
Z==>Y,X==>Z
Y1==>X,Y2==>Z
X==>Z
先給出遞歸算法,這個算法將打印出移動的步驟。
void Hanoi(int n,String X,String Y,String Z) {
if(n == 1) {
System.out.println("將第"+n+"個盤片從"+X+"移到"+Z); //X==>Z
}else {
Hanoi(n-1,X,Z,Y);
System.out.println("將第"+n+"個盤片從"+X+"移到"+Z); //X==>Z
Hanoi(n-1,Y,X,Z);
}
}
複製代碼
漢諾塔的基本要求是要把最大的放在最下面,因此當X
塔座上的盤片數量大於1時,咱們首先要把X
塔座上,1至n-1
的盤片放到Y
上,因而咱們能夠忽略X
最下面的盤片,把Y
當作Z
,Z
當作Y
,這樣問題回到了,當X
有n-1
個盤片,如何移動到Z
的情形。記住這個時候的Z
是Y
。 如今咱們已經作到了把X
上1至n-1
的盤片放到了Y
上。別忘了X
上還有一個咱們忽略的最大盤片。
好,如今把X
最大盤片放到Z
,這個時候X
沒有盤片,Y
有1至n-1
的盤片,如今咱們忽略Z
上的最大盤片。將Y
當作X
,X
當作Y
。問題又回到了X
上有n-1
個盤片如何移動到Z
的情形。
再看上面給出的代碼。當
n==1
時,直接把X
移到Z
,不然,將Y
當作Z
,Z
當作Y
,將1至n-1
盤片移到Y
上。這個時候取出X
最大盤片放到Z
。再把X
當Y
,Y
當作X
,繼續遞歸。直到n==1
,移動成功!其實不難發現當執行到第二個System.out.println("將第"+n+"個盤片從"+X+"移到"+Z);
時。已經完成了將X
的最大盤片移到Z
的目標。漢諾塔的層數減一。問題回到了最初的情形,只不過,盤片在Y
上,不在X
上。可是這不妨礙調用Hanoi
方法。實際上問題的關鍵在於,X,Y,Z
三個塔座沒有區別是同樣的。還有一輪操做完以後,只是把最下面的盤片放到了Z
,這個時候問題的規模就減一了。
上文總結的遞歸特徵中,有一條說明了遞歸必需要有終止條件。好比求階乘,斐波那契數列以及漢諾塔問題,它們在遞歸調用時表示問題規模的n
一直在遞減,直到達到遞歸結束條件n==1
。實際上,遞歸條件不必定是這種經過遞減來達到結束條件判斷值的,好比在構造樹形結構的時候,能夠經過判斷數據項的isLeaf
字段是否爲true
來決定是否退出遞歸。有一點能夠確定的是,遞歸退出條件必定隱藏在初次調用傳入方法的參數中以及一切在方法運行時能夠訪問的狀態。
這是一個比較難的問題,對於相似數學定義問題,循環體每每就是定義問題的那幾句話,好比斐波那契數列的定義:當0<n<3時,Fib=1,當n>2時,Fib(n)=Fib(n-1)+Fib(n-2)
。當爲遞歸數據結構設計算法時,循環體則是訪問遞歸結構的指令。好比遍歷鏈表時:
void foo(node) {
if(node != null) {
foo(node.getNext());
}else {
return;
}
}
複製代碼
可是像漢諾塔這類問題,直觀上沒辦法直接看出遞歸的特徵。我的經驗則是在較小問題規模上,進行演算。發現遞歸特徵。
遞歸程序設計自己是比較難以理解和掌握的,經過簡單的理論學習沒法熟練運用,只有不斷的解決問題纔會熟能生巧。