遞歸詳解

遞歸是一個很重要的概念。看了很多講解之後覺得從2個方面理解比較方便:

1 函數的調用過程。  2.與棧的關係。


從以上兩點講解:

首先必須弄明白遞歸函數是怎麼調用的:每一層的函數都是在上一層遞歸函數結束時才返回的然後接着處理該層遞歸函數剩下的部分

其次與棧的關係:對於每一層的遞歸函數在棧中都保存了本層函數的局部變量,以便該層遞歸函數結束時能夠保存原來該層的數據並使用!




理解了這兩點。在講解兩個例子就理解了:



例1
.給出一個值4267,我們需要依次產生字符‘4’,‘2’,‘6’,和‘7’。就如在printf函數中使用了%d格式碼,它就會執行類似處理。
分析:首先我們會想到用4267取餘,然後除以10再區域,如此循環。但這樣輸出的順序不會是7,6,2,4嗎?於是我們就利用遞歸的堆棧結構的特性:先進後出
  1. public class Recursion{  
  2.     public static void main(String args[]){  
  3.         recursion(4267) ;  
  4.     }  
  5.       
  6.     public static void recursion(int value){  
  7.         int quotient ;  
  8.         quotient = value/10 ;  
  9.         if(quotient!=0){ recursion(quotient) ;}  
  10.         System.out.println(value%10) ;  
  11.     }  
  12. }  

遞歸是如何幫助我們以正確的順序打印這些字符呢?下面是這個函數的工作流程。


       1. 將參數值除以10
       2. 如果quotient的值爲非零,調用binary-to-ascii打印quotient當前值的各位數字
  3. 接着,打印步驟1中除法運算的餘數


  注意在第2個步驟中,我們需要打印的是quotient當前值的各位數字。我們所面臨的問題和最初的問題完全相同,只是變量quotient的 值變小了。我們用剛剛編寫的函數(把整數轉換爲各個數字字符並打印出來)來解決這個問題。由於quotient的值越來越小,所以遞歸最終會終止。
  一旦你理解了遞歸,閱讀遞歸函數最容易的方法不是糾纏於它的執行過程,而是相信遞歸函數會順利完成它的任務。如果你的每個步驟正確無誤,你的限制條件設置正確,並且每次調用之後更接近限制條件,遞歸函數總是能正確的完成任務。
  但是,爲了理解遞歸的工作原理,你需要追蹤遞歸調用的執行過程,所以讓我們來進行這項工作。追蹤一個遞歸函數的執行過程的關鍵是理解函數中所聲 明的變量是如何存儲的。當函數被調用時,它的變量的空間是創建於運行時堆棧上的。以前調用的函數的變量扔保留在堆棧上,但他們被新函數的變量所掩蓋,因此 是不能被訪問的。
  當遞歸函數調用自身時,情況於是如此。每進行一次新的調用,都將創建一批變量,他們將掩蓋遞歸函數前一次調用所創建的變量。當我追蹤一個遞歸函數的執行過程時,必須把分數不同次調用的變量區分開來,以避免混淆。
  程序中的函數有兩個變量:參數value和局部變量quotient。下面的一些圖顯示了堆棧的狀態,當前可以訪問的變量位於棧頂。所有其他調用的變量飾以灰色的陰影,表示他們不能被當前正在執行的函數訪問。
假定我們以4267這個值調用遞歸函數。當函數剛開始執行時,堆棧的內容如下圖所示:
 



執行除法之後,堆棧的內容如下:

  
接着,if語句判斷出quotient的值非零,所以對該函數執行遞歸調用。當這個函數第二次被調用之初,堆棧的內容如下:
 


堆棧上創建了一批新的變量,隱藏了前面的那批變量,除非當前這次遞歸調用返回,否則他們是不能被訪問的。再次執行除法運算之後,堆棧的內容如下:
 


quotient的值現在爲42,仍然非零,所以需要繼續執行遞歸調用,並再創建一批變量。在執行完這次調用的出發運算之後,堆棧的內容如下:
 

此時,quotient的值還是非零,仍然需要執行遞歸調用。在執行除法運算之後,堆棧的內容如下:
 

 
  不算遞歸調用語句本身,到目前爲止所執行的語句只是除法運算以及對quotient的值進行測試。由於遞歸調用這些語句重複執行,所以它的效果 類似循環:當quotient的值非零時,把它的值作爲初始值重新開始循環。但是,遞歸調用將會保存一些信息(這點與循環不同),也就好是保存在堆棧中的 變量值。這些信息很快就會變得非常重要。
  現在quotient的值變成了零,遞歸函數便不再調用自身,而是開始打印輸出。然後函數返回,並開始銷燬堆棧上的變量值。
每次調用putchar得到變量value的最後一個數字,方法是對value進行模10取餘運算,其結果是一個0到9之間的整數。把它與字符常量‘0’相加,其結果便是對應於這個數字的ASCII字符,然後把這個字符打印出來。
   輸出4: 
 


接着函數返回,它的變量從堆棧中銷燬。接着,遞歸函數的前一次調用重新繼續執行,她所使用的是自己的變量,他們現在位於堆棧的頂部。因爲它的value值是42,所以調用putchar後打印出來的數字是2。
  輸出42: 
 


接着遞歸函數的這次調用也返回,它的變量也被銷燬,此時位於堆棧頂部的是遞歸函數再前一次調用的變量。遞歸調用從這個位置繼續執行,這次打印的數字是6。在這次調用返回之前,堆棧的內容如下:
  輸出426:
 


現在我們已經展開了整個遞歸過程,並回到該函數最初的調用。這次調用打印出數字7,也就是它的value參數除10的餘數。
  輸出4267:
 


然後,這個遞歸函數就徹底返回到其他函數調用它的地點。
如果你把打印出來的字符一個接一個排在一起,出現在打印機或屏
幕上,你將看到正確的值:4267 




例2 倒序輸出

  1. #include<iostream>  
  2. #include<string>  
  3. using namespace std;  
  4.   
  5. int i=0,j;  
  6. void reverse(string &s);  
  7. int main()  
  8. {  
  9.     string s;  
  10.   
  11.     cin>>s;  
  12.     j=i=s.size();  
  13.     reverse(s);  
  14.     cout<<s<<endl;  
  15.   
  16.     return 0;  
  17. }  
  18.   
  19. void reverse(string &s)  
  20. {  
  21.     char ch;                    //..........第一部分 ..........  
  22.     i--;  
  23.     ch=s[i];  
  24.     cout<<ch<<endl;             //這裏i是全局變量,而ch是局部變量會保存在棧中  
  25.     if(-1==i)  
  26.         return;                    
  27.     reverse(s);                 //本身的遞歸看做第二部分  
  28.                                 //後續部分看做第三部分  
  29.     s[--j]=ch;               //這句當且僅當該遞歸函數中的reverse返回時 才執行  
  30.     cout<<ch<<endl;  
  31. }  
在以上代碼中,每一層只有當reverse()結束了纔會接着處理下面的s[--j]=ch;代碼,因爲每一次遞歸進去的時候reverse()上面的代碼都已經處理了,所以當遞歸返回時處理的自然就是reverse()下面的代碼了,如此循環直到結束!不過我覺着最重要的還有一樣就是有時候不必刻意去關注的那麼細,也要有全局觀,例如我們只需要知道函數reverse()是繼續處理同樣的功能,沒必要再去想這個函數裏面又是怎麼樣怎麼樣的,我感覺肯定會抓狂的!希望跟我一樣糾結的朋友不在糾結遞歸了.........





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