下圖描述的是從問題引出到問題變異的思惟過程:函數
本文以數制轉換爲引,對遞歸進行分析。主要是從多角度分析遞歸過程及討論遞歸特色和用法。oop
一次在完成某個程序時,忽然想要實現任意進制數相互轉換,因而就琢磨,至少涉及如下參數:學習
遞歸就是遞歸函數。遞歸函數是直接或間接調用自身的函數。
舉個例子:測試
1 /* 2 ** 接受一個整型值(無符號),把它轉換爲字符並打印它,前導零被刪除。 3 */ 4 #include <stdio.h> 5 void binary_to_ascii( unsigned int value ) { 6 unsigned int quotient; 7 quotient = value / 10; 8 if( quotient != 0) 9 binary_tc_ascii( quotient ); 10 putchar( value % 10 + '0' ); 11 }
另外遞歸還有所謂「三個條件」,「兩個階段」。我就不說了。實際應用時通常都很天然的知足條件。
spa
2. 遞歸過程分析
中斷角度
看例:
有5人從左至右坐,右邊人的年齡比相鄰左邊人大2歲,最左邊的那我的10歲。問最右邊人年齡。
程序2: age.c
1 #include <stdio.h> 2 age(int n) { 3 int c; 4 if( n == 1 ) 5 c = 10; 6 else 7 c = age( n-1 ) + 2; 8 return(c); 9 } 10 11 int main() { 12 printf("%d\n\n",age( 5 ) ); 13 return 0; 14 }
表達式:
遞推和回推過程:
設計
這跟中斷有什麼聯繫呢?如今看來確實不很明顯,不過最初我就是由它想到《微機原理》中的中斷的:從age(5)開始執行,而後調用age(4),即來一箇中斷,此時先保護現場,而後一直遞歸直到n=1時,中斷結束,而後層層返回,也就是不斷恢復現場的過程。3d
嵌套調用角度:
嵌套調用關係圖:
看懂了這個圖,把上面的fun_a()和fun_b()全換成同樣的fun(),就至關因而遞歸時的函數對自身的調用過程。
另外好像這幅圖更容易看出「中斷過程」吧。指針
堆棧角度code
若是中斷和嵌套這兩個角度都看明白的話,這個堆棧角度就是昇華一下。blog
還用程序1爲例進行分析:
程序1的函數有兩個變量:參數value和局部變量quotient。下面的一些圖顯示了堆棧的狀態,當前能夠訪問的變量位於棧頂。全部其餘調用的變量飾以灰色陰影,表示它們不能被當前正在執行的函數訪問。
假定咱們以4267這個值調用遞歸函數。當函數開始執行時,堆棧的內容以下圖所示。
執行除法運算以後,堆棧的內容以下:
接着,if語句判斷出 quotient 的值非零,因此對該函數執行遞歸調用。當這個函數第二次被調用之初,堆棧的內容以下:
堆棧上建立了一批新的變量,隱藏了前面的那批變量,除非當前此次遞歸調用返回,不然它們是不能被訪問的。再次執行除法運算以後,堆棧的內容以下:
quotient的值如今爲42,仍然非零,因此須要繼續執行遞歸調用,並再建立一批變量。在執行完此次調用的除法運算以後,堆棧的內容以下:
此時,quotient的值仍是非零,仍然須要執行遞歸調用。在執行除法運算以後,堆棧的內容以下:
不算遞歸調用語句自己,到目前爲止所執行的語句只是除法運算以及對quotient的值進行測試。因爲遞歸調用使這些語句重複執行,因此它的效果相似循環:當quotient的值非零時,把它的值做爲初始值從新開始循環。可是,遞歸調用將會保存一些信息(這點與循環不一樣),也就是保存在堆棧中的變量值。這些信息很快就會變得很是重要。
如今quotient的值變成了零,遞歸函數便再也不調用自身,而是開始打印輸出。而後函數返回,並開始銷燬堆棧上的變量值。
每次調用putchar獲得變量value的最後一個數字,方法是對value進行模10餘運算,其結果是一個0~9之間的整數。把它與字符常量'0'相加,其結果即是對應於這個數字的ASCII字符,而後把這個字符打印出來。
接着函數返回,它的變量從堆棧中銷燬。接着,遞歸函數的前一次調用從新繼續執行,它所使用的是本身的變量,它們如今位於堆棧的頂部。由於它的value值是42,因此調用putchar後打印出來的數字是2 。
接着遞歸函數的此次調用也返回,它的變量也被銷燬,此時位於堆棧頂部的是遞歸函數再前一次調用的變量。遞歸調用從這個位置繼續執行,此次打印的數字是6 。在此次調用返回以前,堆棧的內容以下:
如今咱們已經展開了整個遞歸過程,並回到該函數最初的調用。此次調用打印出數字7,也就是它的value參數除10的餘數。
而後,這個遞歸函數就完全返回到其餘函數調用它的地點。
若是你把打印的字符一個接一個排在一塊兒,出如今打印機或屏幕上,你將看到正確的值4267 。
上面從不一樣角度對遞歸過程進行了分析。而際應用時並不要求你搞清楚每一個遞歸的內部過程,重要的是用對。
下面主要是不恰當應用遞歸的一些例子:
許多教材中都把計算階乘和菲波那契數列用來講明遞歸,然而前者中遞歸併無提供任何優越之處,後者中遞歸的效率很是之低。
看一下極端的菲波那契數求解:
表達式:
這種遞歸形式的定義容易誘導人們使用遞歸形式來解決問題:
1 /* 2 ** 用遞歸方法計算第n個菲波那契數列的值。 3 */ 4 5 int fibonacci( int n ) { 6 if( n <= 2 ) 7 return 1; 8 return fibonacci( n - 1 ) + fibonacci( n - 2 ); 9 }
這裏有一個陷阱:它使用遞歸步驟計算fibonacci( n -1)和 fibonacci( n -2)。可是,在計算 fibonacci( n -1)時也將計算 fibonacci( n -2)。這個額外的代價有多大呢?
答案是:它的代價遠遠不止一個冗餘計算:每一個遞歸調用都會觸發另外兩個遞歸調用,面這兩個調用的任何一個還並將觸發兩個遞歸調用,再接下去的調用也是如此。這樣,冗餘計算的數量增加得很是快。例如,在遞歸計算fibonacci(10)時,fibonacci(3)的值被計算了21次。可是在遞歸計算fibonacci(30)時,fibonacci(3)的值被計算了317811次,固然,這317811次產生的結果是徹底同樣的,除了其中之一外,其他的純屬浪費。
想得更極端一些,假如你在程序中遞歸時不是兩次而是3次,4次,更屢次的調用自身,那我想可能會讓程序崩潰吧。
如今讓咱們嘗試用循環代替遞歸:
程序4:fib_iter.c
1 int fibonacci( int n ) { 2 int result; 3 int previous_result; 4 int next_older_result; 5 result = previous_result = 1; 6 while(n > 2 ) { 7 n -= 1; 8 next_older_result = previous_result; 9 previous_result = result; 10 result = previous_result + next_older_result; 11 } 12 return result; 13 }
OK,說到這了,本文引子是數制轉換,總得說點數制轉換點題是吧。
嗯,把題目都忘記了,回引子看一下吧。
1 #ifndef _CONERT_H 2 #define _CONERT_H 3 #include <stdio.h> 4 #include <math.h> 5 #endif 6 7 /* 8 **main() 9 */ 10 11 int conert2any( int scr, int dest_d, int pow_base ) { 12 /* 13 ** 調用該函數時參數pow_base必須爲0 14 */ 15 int quotient, result; 16 int dest_d_base = 10; 17 quotient = scr / dest_d; 18 if( quotient != 0 ) 19 result = ( scr % dest_d ) * pow( dest_d_base, pow_base) + conert2any( quotient, dest_d, ++pow_base ); 20 else 21 result = ( scr % dest_d ) * pow( dest_d_base, pow_base); 22 return ( result ); 23 }
OK,這個數制轉換程序用遞歸實現,沒什麼問題,但受上例啓發它也能夠改成循環:
1 do { 2 result += (scr % dest_d ) * pow( dest_d_base, pow_base++ ); 3 } while( scr /= dest_d != 0 )
相比於遞歸,它更短小精悍,效率也高些。
通過兩個遞歸改成循環的例子,你應該發現這兩個例子有一個共同點:遞歸調用時最後執行的語句是return 。
對於這種調用時最後執行的是return的遞歸,有一種專門的稱呼:尾部遞歸。
能夠發現通常狀況下尾部遞歸均可以改成相應的循環形式,並且更簡潔高效。
那何時才必須用遞歸呢?據我目前的經驗和思考,只有程序1--逆序打印是必須的,其它好像沒有必須用遞歸的。
好了,到這遞歸也告一段落了,來個小插曲,談一下我寫程序5時的一些感覺:
實現這個進制轉換函數時,對遞歸的理解還不深,犯了如今看來好笑的錯誤:其中要用遞歸實現加權求和,我還曾苦思如何實現累加呢,每一次調用完後變量都銷燬了,如何累加呢?苦思的結果是:利用靜態變量保存累加的值。若是到此爲止的話我也不會進一步學習遞歸。由於我想,雖然這樣能實現,但是不完美,即使碧波函數調用完了,靜態變量依然在佔着空間,並且再次調用前還得先清零。C語言的遞歸不應是如此麻煩的,必定是我哪裏想差了,因而我就反覆看書上的例子,終於醒悟:直接用return返回不就能夠實現累加了嘛。唉,當時腦子真是灌了漿糊了。
言歸正傳,全文結束,對遞歸總結一下:
說明:
date: 2014-12-10