C語言遞歸分析

思路

下圖描述的是從問題引出到問題變異的思惟過程:函數

概述

本文以數制轉換爲引,對遞歸進行分析。主要是從多角度分析遞歸過程及討論遞歸特色和用法。oop

引子

一次在完成某個程序時,忽然想要實現任意進制數相互轉換,因而就琢磨,至少涉及如下參數:學習

  1. 源進制數:scr
  2. 目標進制:dest_d
    實現的大體思路:
    scr --> 數字分解 --> 按權求和 --> dest
    很明顯這個過程是先正序分解,而後逆序求和,因此我就聯想到了遞歸。

遞歸

1. 遞歸的含義

  1. 遞歸就是遞歸函數。遞歸函數是直接或間接調用自身的函數。

舉個例子:測試

程序1: btoa.c
 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歲。問最右邊人年齡。
    1. 程序2: age.c
    2.  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這個值調用遞歸函數。當函數開始執行時,堆棧的內容以下圖所示。
    e1.png
      執行除法運算以後,堆棧的內容以下:
    e2.png
      接着,if語句判斷出 quotient 的值非零,因此對該函數執行遞歸調用。當這個函數第二次被調用之初,堆棧的內容以下:
    e3.png
      堆棧上建立了一批新的變量,隱藏了前面的那批變量,除非當前此次遞歸調用返回,不然它們是不能被訪問的。再次執行除法運算以後,堆棧的內容以下:
    e4.png
      quotient的值如今爲42,仍然非零,因此須要繼續執行遞歸調用,並再建立一批變量。在執行完此次調用的除法運算以後,堆棧的內容以下:
    e5.png
      此時,quotient的值仍是非零,仍然須要執行遞歸調用。在執行除法運算以後,堆棧的內容以下:
    e6.png
      不算遞歸調用語句自己,到目前爲止所執行的語句只是除法運算以及對quotient的值進行測試。因爲遞歸調用使這些語句重複執行,因此它的效果相似循環:當quotient的值非零時,把它的值做爲初始值從新開始循環。可是,遞歸調用將會保存一些信息(這點與循環不一樣),也就是保存在堆棧中的變量值。這些信息很快就會變得很是重要。
      如今quotient的值變成了零,遞歸函數便再也不調用自身,而是開始打印輸出。而後函數返回,並開始銷燬堆棧上的變量值。
      每次調用putchar獲得變量value的最後一個數字,方法是對value進行模10餘運算,其結果是一個0~9之間的整數。把它與字符常量'0'相加,其結果即是對應於這個數字的ASCII字符,而後把這個字符打印出來。
    t1.png
      接着函數返回,它的變量從堆棧中銷燬。接着,遞歸函數的前一次調用從新繼續執行,它所使用的是本身的變量,它們如今位於堆棧的頂部。由於它的value值是42,因此調用putchar後打印出來的數字是2 。
    t2.png
      接着遞歸函數的此次調用也返回,它的變量也被銷燬,此時位於堆棧頂部的是遞歸函數再前一次調用的變量。遞歸調用從這個位置繼續執行,此次打印的數字是6 。在此次調用返回以前,堆棧的內容以下:
    t3.png
      如今咱們已經展開了整個遞歸過程,並回到該函數最初的調用。此次調用打印出數字7,也就是它的value參數除10的餘數。
    t4.png
      而後,這個遞歸函數就完全返回到其餘函數調用它的地點。
      若是你把打印的字符一個接一個排在一塊兒,出如今打印機或屏幕上,你將看到正確的值4267 。

  • 3. 遞歸的應用

      上面從不一樣角度對遞歸過程進行了分析。而際應用時並不要求你搞清楚每一個遞歸的內部過程,重要的是用對。
      下面主要是不恰當應用遞歸的一些例子:
      許多教材中都把計算階乘和菲波那契數列用來講明遞歸,然而前者中遞歸併無提供任何優越之處,後者中遞歸的效率很是之低。
      看一下極端的菲波那契數求解:
      表達式:
      
      這種遞歸形式的定義容易誘導人們使用遞歸形式來解決問題:

    程序3:fib_rec.c
    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次產生的結果是徹底同樣的,除了其中之一外,其他的純屬浪費。
    1.   想得更極端一些,假如你在程序中遞歸時不是兩次而是3次,4次,更屢次的調用自身,那我想可能會讓程序崩潰吧。
    2.   如今讓咱們嘗試用循環代替遞歸:
    3. 程序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 }

  1.   OK,說到這了,本文引子是數制轉換,總得說點數制轉換點題是吧。
 嗯,把題目都忘記了,回引子看一下吧。
程序5:convert.c
 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,這個數制轉換程序用遞歸實現,沒什麼問題,但受上例啓發它也能夠改成循環:

程序6:convert_loop.c
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返回不就能夠實現累加了嘛。唉,當時腦子真是灌了漿糊了。


 

言歸正傳,全文結束,對遞歸總結一下:

  1. 遞歸便是函數對自身的嵌套調用。
  2. 通常狀況下尾部遞歸是沒必要要的,用循環會更好。
  3. 用遞歸分析重複過程井井有條,因此最好用先用遞歸分析,而後轉用循環去實現。

     


說明:

  1. 程序1,3,4 引自《C和指針》7.5
  2. 程序2 引自 本校教材《C語言程序設計》7.4
  3. 「堆棧角度」 引自 《C和指針》7.5

     

date: 2014-12-10

相關文章
相關標籤/搜索