談談遞歸的理解
以下爲個人理解,可能 對可能不對,有參考價值借點贊
遞歸專業解釋爲:是一種調用自身的編程技術。看起來比較簡單,不就調用自身嘛,在函數體裏調用自己的函數就是遞歸技術,注意設計遞歸結束的條件不然將會無限遞歸導致內存溢出。僅僅這樣理解的話除了能看別人寫的遞歸巧妙算法外似乎感覺自己沒用過遞歸,自己不知道什麼時候用遞歸只是發現別人用遞歸時覺得“美妙原來還可以這樣寫”,根據別人的思路自己寫遞歸還會繞來繞去自己都被繞暈了,還是copy別人的吧。所以最主要的是理解其中的思想,從深層次的思想出發進行解決問題才能轉化爲自己的東西。
遞歸思想:遞歸實際爲一種分而治之的思想( 分治算法),先把大問題轉化爲小問題,進而對小問題逐個解決合併爲原問題的解決,既然能遞歸解決,那麼大問題和劃分的小問題解決的模型應該是一樣的才能調用自己的函數,稱爲遞歸模型,使用遞歸思想編程者就是在建立這樣的模型成爲遞歸模型。遞歸過程爲“遞”和“歸”兩個過程,先是遞即將大的問題通過一定的調用轉遞給解決問題的更小問題(把大化小的過程),達到一定條件後遞結束,到歸階段即將問題逐個解決的過程(先解決小問題進而合併解決大問題)。(遞而不歸的不屬於遞歸算法,應該屬於動態規劃啥的。所以一定是一種能遞能歸的模型)
步驟:既然知道了遞歸爲大化小,小擊大這麼一個過程,而在編程中又有允許函數調用自身這麼一個技術支持,接下來就是使用者的工作,如何把大問題化小從而可以達到以小擊大的效果呢?這個過程爲遞歸模型的建立。這樣一個模型(即編寫的函數)應該有怎樣的條件呢?既能把問題一層一層的劃分爲更小的問題,而且能一層一層的解決小問題合併成原問題的解,劃分很多種但是必須同時能合併解決,所以從能把小問題合併爲大問題的方面入手,總結出能**“歸”並出大問題的解決的辦法,這個思維的難點就是要編程者調出原來思考方式的慣性,從另一個角度思考,所以會感覺非常的怪怪的所以就是難點,以往的思考點是,從大問題出發小解決一部分呢然後丟出來更小的問題去解決(這裏舉例快速排序,快速排序之所很容易就理解和編寫,沒有感覺繞來繞去的是因爲符合我們通常思考的方式,先劃分爲兩部分在劃分兩部分的基礎上再更小部分,當更小部分解決完了就排序完了,沒有迴歸的過程,所以好理解編碼也不怎麼繞暈),遞歸解決問題的思考方式是先把大問題化爲小問題先解決小問題再在小問題的基礎上解決更大一點的問題進而一層層大問題解決**(像遞歸排序一樣,先排序小的部分,再在小部分排序好的基礎上排序更大的部分,最後排序整個數列)。這種從大問題逆向思考是比較費勁的分分鐘繞暈個人,不過這裏提供一個好的數學工具-----數學歸納法
借鑑自《遞歸與數學歸納》
數學歸納法,想必每一個人都應該學習了數學歸納法,當我們需要去證明一個證明題時,很可能就要用到數學歸納法,數學歸納法的思想如下:
一般地,證明一個與自然數n有關的命題P(n),有如下步驟:
(1)證明當n取第一個值n0時命題成立。n0對於一般數列取值爲0或1,但也有特殊情況;
(2)假設當n=k(k≥n0,k爲自然數)時命題成立,證明當n=k+1時命題也成立。
綜合(1)(2),對一切自然數n(≥n0),命題P(n)都成立。
其實,數學歸納法利用的是遞推的原理,形象地可以叫做多米諾原理。因爲N+1的成立就可以向前向後遞推所有數都成立。
而遞歸利用的也是遞推的原理,在整個程序中,反覆實現的將是同一個原理。在這一點上,遞歸與數學歸納法本質是相同的。數學歸納法就是正向思考遞歸這種逆向實現的方法,從最小問題出發找解決辦法而不是從大問題直接思考,感受到了計算機和數學的關係,計算機就是數學的兒子……所以可以利用數學歸納法來設計遞歸的實現。
用歸納法設計遞歸程序的步驟:
一、用數學歸納法分析問題,根據數學歸納法的第一步得出截至部分。
二、根據數學歸納法的第二步來構造函數的遞歸部分。其實這個求解過程就是找出R(N)與R(N-1)的關係式。
用遞歸解決漢諾塔問題
假設n個盤子從from藉助temp移動to,最上面爲第一個
設想一下,如果只有一個盤子的話直接從from移動到to就可以了
如果兩個盤子的話先將第一個盤子移動到temp上再將第二個盤子移動to再將temp移動到to上
三個盤子的話…………
從另外一個角度思考,其實每次移動我們都是會先思考如何把當前最上面的移到to上,就是說思考的方式是先移動第一個再移動第二個再第三,然後發現來來去去都是要把這個移到那裏的下面先把那裏先移動出來再移動回去,然後發現來來去去都是要把這個移走先移動一個小的方一邊再移動大的放一邊再把小的放到大的上面,再移動大的……發現了重複枯燥的一面,剛剛玩還覺得新鮮,玩着玩着發現來來去去都是那樣最多就是盤子越來越多就是重複拉來拉去……而計算就是擅長這種無聊重複的工作。上面說的無論盤子多少規則都是一樣的,就是說給了一種遞推的可,就說移動第二個是在移走第一個的基礎上,把第一個移走了就直接把第二個移動過去,第三個是在把第二個和第一個都移走了就直接移動過去……
每個盤子要移動到目的柱子上,都必須依賴於上面的盤子先全部移動到輔組的柱子上這就是遞歸的思想,先把大問題化爲小問題先解決小問題再在小問題的基礎上解決更大一點的問題進而大問題解決。從另外一個方面思考,不是想着從第一個再到第二個……一直移動到最後一個,而是想着要移動最後一個就是最大那個,先把上面的全部移動到temp的柱子上去,再把最大的移動到to上,要把上面的移動到temp的柱子上去,不能直接移動所以只能一個一個移動,先移動最下面藉助to移動到temp……直到只有一個時才能直接移動再回歸,就是遞歸的條件。這種思路時很難想到的,因爲不屬於常規思維,想到就時狠人級別的人物。下面用歸納法分析。
首先用數學歸納法分析:
1、當只移動一個盤子時,我們可以確定唯一動作:直接將圓盤從from移動到to上。
2、現在假設移動n個盤子,而我們也可以將這些圓盤最終按要求移動到to上,當然也可以移動到temp上(只是假設,這是歸納法假設下一步證明)
可以輕易的證明如果n+1個盤子我們也可以將圓盤全部按要求移動到to上,因爲我們可以先將上面的N個移動到temp上(第二步已假設成立),再把剩下的最後一個移動到to上,再把temp上的移動到to上(這些都已經假設可以成立)。
這裏可能會懵逼這裏用歸納法證明了 啥
證明了一個函數可以實現漢諾塔問題,這個函數特徵爲:
當盤子數量爲1時,直接對第1個執行移動move(1,from,to)從from直接移動到to
如果盤子數量不爲n時,先將n-1個移動到temp,然後直接對移動第n個執行移動move(n,from,to)從from直接移動到to,再將temp上的n-1個移動到to上。
先將n-1個移動到temp和再將temp上的n-1個移動到to上即是同樣的問題,調用自身函數即可。翻譯成Java代碼如下,如此簡潔。。
public void hanoi(int n,String from,String temp,String to){
if(n==1){
System.out.println(1+":"+from+"->"+to);
return;
}
hanoi(n-1,from,to,temp);
System.out.println(n+":"+from+"->"+to);
hanoi(n-1,temp,from,to);
}
階乘問題
這個問題如果用循環也很難容易
int n=6;//假設爲6的階乘
int a=1;
while(n>0){
a=a*n;
n--;
}
不過這裏說一下遞歸的解法思路,階乘算法很典型的思考小問題解決合併成大問題的情況,拿到一個數的階乘很容易就想到直接算從1乘到n解出來,如果解不出來就是一直想辦法怎麼可以從1乘到n,很難跳出思維的怪圈想到n的階乘和n-1的階乘的關係從而用遞歸解決。這種情況可以試想一下和上一個階乘的關係用遞歸。
當是1時直接是1
當爲2時爲12
當爲3時爲12*3
如果爲n的階乘知道了
那麼n+1的階乘就是再乘以一個n+1就可以,遞推方程式就出來了
public int factor(n){
if(n==1)
return 1;
else
return(n*factor(n-1));
}
消除遞歸
遞歸對於分析問題比較有優勢,但是基於遞歸的實現效率就不高了,而且因爲函數棧大小的限制,遞歸的層次也有限制。消除遞歸,這樣可以在分析階段採用遞歸思想,而實現階段採用非遞歸算法。
遞歸即先把大問題化爲小問題先解決小問題再在小問題的基礎上解決更大一點的問題進而一層層大問題解決,如果能保存小問題的解決結果那麼就可以不用調用自身解決大問題了,消除遞歸有的可以通過循環的方式就可以消除,例如上面的階乘實現,不過一般情況下是不可以的,因爲如果可以用循環實現就一般不會用遞歸了。之所以用遞歸是因爲遞歸過程可以保存上一步的結果,也就是說在前人的基礎之上進行的所以代碼才簡潔。如果通過其他方式保存上一步保存的結果那麼就用不上調用自己的函數了,提高效率,比如上面的階乘在循環裏用一個變量保存上一次循環的計算結果,消除調用自己的函數就是要想辦法保存上一步的結果。基本上函數的調用系統上都是基於棧,每次調用都涉及如下操作:
調用開始時:將返回地址和局部變量入棧。
調用結束時:出棧並將返回到入棧時的返回地址
自然的想到我們也可以用棧來實現遞歸的消除。那麼什麼時候入棧什麼時候出棧,棧保存什麼呢,
看一下如果用棧來消除遞歸的話
//要存儲的數據
class Data{
//方法參數
private int n;
Data(int n){
this.n=n;
}
public int getData(){
return n;
}
}
//棧
Stack<Data> myStack = new Stack<>();
//狀態機實現運算過程
public static int execute(int num){
int i = 1;
int result = 1;
while(i!=6){ //結束
switch(i){
case 1: //初始化
i=2;
break;
case 2: //條件是否結束
if(num==1){
result=1;
i=4;
}else{
i=3;
}
break;
case 3: //遞歸入棧
Data data = new Data(num);
myStack.push(data);
num--; //條件發生變化
i=2;
break;
case 4: //棧是否空
if(myStack.isEmpty()){
i=6;
}else{
i=5;
}
break;
case 5: //回溯出棧
Data data1 = myStack.pop();
result*=data1.getData();
i=4;
break;
}
}
return result;
}
顯然模仿函數的調用過程即可,思想還是遞歸的思想只是實現的方式從調用函數轉變爲棧的出入和執行的跳轉,回答上面的問題,入棧的是當前環境的參數包括變量和執行的位置,出棧後改變環境參數下面用遞歸思想非調用自身實現漢諾塔問題,標註很清楚可以而且可以直接執行。
import java.util.Stack;
/**
* 這裏用頁面來表示每個棧裏的數據項
* 每入棧一次,相當於翻頁一次,像書一樣好理解
* 每出棧一次相當於往回翻書一樣
* 當要進入下一頁面時,先保存當前頁面信息,再進入
* 出棧即將之前的頁面信息彈出,改變當前的參數
*
* 這樣子想象容易理解而已,其實所謂的翻頁就是參數的變化而已
* 說是頁面翻頁還不如直接說參數變成啥啥的,只是這樣子不好理解
* 不同的頁面表現出來就是參數的不一樣,頁面唯一表示可以用num即n表示
*
* **/
class Hanoi{
//只是用來記步驟,和實現無關
int count=1;
public static void main(String[]u){
Hanoi han =new Hanoi();
//這個是遞歸實現的,放這裏對比錯誤,看非遞歸結果是否正確
han.hanoi1(4,"A","B","C");
System.out.println("-------------------");
han.count=0;
han.hanoi(4,"A","B","C");
}
class Data{
private int num; //保存頁面棧,表示當前處於那個頁面
private String from; //保存當前頁面需要移動的起始
private String temp;//輔助
private String to;//目的
private int location;//當前頁面執行到哪個位置
Data(int n,String from,String temp,String to,int location){
num=n;
this.from=from;
this.temp=temp;
this.to=to;
this.location=location;
}
public int getNum() {
return num;
}
public String getFrom() {
return from;
}
public String getTemp() {
return temp;
}
public String getTo() {
return to;
}
public int getLocation() {
return location;
}
}
public void hanoi1(int n,String from,String temp,String to){
if(n==1){
System.out.println(count+++"|"+1+":"+from+"->"+to);
return;
}
hanoi1(n-1,from,to,temp);
System.out.println(count+++"|"+n+":"+from+"->"+to);
hanoi1(n-1,temp,from,to);
}
public void hanoi(int n,String from,String temp,String to){
//存頁面數據的棧
Stack<Data> myStack1 = new Stack<>();
int i=1;//用來做狀態轉變的,即執行什麼步驟
String temp1;
while(i!=4){
switch(i){
case 1:
if(n==1){
//當爲最後一頁時,即只有一個盤時,和遞歸部分一樣
System.out.println(count+++"||"+n+":"+from+"->"+to);
i=3;//轉到3處,即轉到彈出棧,返回前一個頁面的地方
}
else{
//否則轉到2,繼續入棧
i=2;
}
break;
case 2:
//將當前頁面信息入棧,這裏執行到第一個函數
myStack1.push(new Data(n,from,temp,to,1));
//當前頁面信息入棧後,將頁面翻到下一頁,(即將改變參數)(相當於遞歸的hanoi1(n-1,from,to,temp);)
n--;
temp1=to;
to=temp;
temp=temp1;
i=1;//然後進入到另外一個頁面相當於遞歸從頭執行函數
break;
case 3:
//空棧即爲結束
if (myStack1.isEmpty()){
i=4;
break;
}
else {
//往回翻一個頁面(彈出頁面信息用於修改參數)
Data data = myStack1.pop();
//即爲之前執行到第一個函數後入棧的相當於遞歸的執行到hanoi1(n-1,from,to,temp);
if (data.getLocation()==1) {
//相當於遞歸的System.out.println(count+++"|"+n+":"+from+"->"+to);
System.out.println(count+++"||"+data.getNum() + ":" + data.getFrom() + "->" + data.to);
//用棧裏的信息更新參數(翻頁了參數變)
n=data.getNum();
from = data.getFrom();
temp = data.getTemp();
to=data.getTo();
//執行到另外的函數,相當於執行到遞歸裏的hanoi1(n-1,temp,from,to);
//將當前頁面信息入棧,這裏和上面不一樣,因爲執行到了不一樣的地方
myStack1.push(new Data(n, from, temp, to,2));
//當前頁面信息入棧後,將頁面翻到下一頁,(即將改變參數)(相當於遞歸的hanoi1(n-1,temp,from,to))
n--;
temp1 = from;
from = temp;
temp = temp1;
i = 1;//然後進入到另外一個頁面相當於遞歸從頭執行函數
}
//如果location==2的話說明上次入棧時爲執行第二個函數,直接結束當前函數,即再彈出前一個頁面的棧
else{
i=3;
}
break;
}
}
}
}
}