9.3.1 遞歸的使用算法
爲了具體說明,請看下面的例子。程序清單9.6中函數main()調用了函數up_and_down()。咱們把此次調用稱爲「第一級遞歸」。而後up_and_down()調用其自己,此次調用叫作「第二級遞歸」。第2級遞歸調用第3級遞歸,依此類推。爲了深刻其中看看究竟發生了什麼, 程序不只顯示出了變量n的值,還顯出出了存儲n的內存的地址&n(本章稍後部分將更全面的討論&運算符。printf()函數使用%p說明符來指示地址)。編程
程序清單9.6 recur.c程序函數
/*recur.c --遞歸舉例*/ #include <stdio.h> void up_and_down(int); int main(void) { up_and_down(1); return 0; } void up_and_down (int n) { printf("Level %d : n location %p\n",n,&n); /*1*/ if(n<4) up_and_down(n+1); printf("Level %d : n location %p\n",n,&n); /*2*/ }
咱們來分析程序中遞歸的具體工做過程。首先main()使用參數1調用了函數up_and_down()。因而up_and_down()中形式參量n的值爲1,故打印語句#1輸出了Leve 1。而後,因爲n的數值小於4,因此up_and_down()(第1級)使用n+1即數值2調用了up_and_down()(第2級)。這使得n在第2級調用中被賦值2,打印語句#1輸入的是Level 2。與之相似,下面的再次調用分別打印出Level 3和Level 4。oop
當開始執行第4級調用時,n的值是4,所以if語句的條件不知足。這時再也不繼續調用up_and_down()函數。第4級調用接着執行打印語句#2,即輸出Level 4,由於n的值是4。如今函數須要執行return語句,此時第4級調用結束,把控制返回給該函數的調用函數,也就是第3級調用函數。第3級調用函數中前一個執行事後語句是在if語句中進行第4級調用。所以,它開始繼續執行其後續的代碼,即執行打印語句#2,這將會輸出Level 3.當第3級調用結束後,第2級調用函數開始繼續執行,即輸出Level 2。依此類推。學習
注意,每一遞歸都使用它本身私有的變量n。你能夠經過查看地上的值來得出這個結論(固然,不一樣的系統一般會以不一樣的格式顯示不一樣的地址。關鍵點在於,調用時的Level 1地址和返回時的Level 1地址是相同的)。ui
若是您對此感到有些迷惑,能夠假想進行了一系列的函數調用,即便用fun1()調用了fun2()、fun2()調用fun3(),fun3()調用fun4()。fun4()執行完後,fun3()會繼續執行。而fun3()執行完後,開始執行fun2()。最後fun2()返回到fun1()中並執行後續的代碼。遞歸過程也是如此,只不過fun1() fun2() fun3() fun4()都是相同的函數。編碼
9.3.2 遞歸的基本原理code
剛接觸遞歸可能會感到迷惑,下面將講述幾個基本要點,以便於理解該過程:遞歸
第一,每一級的函數調用都有本身的變量。也就是說,第1級調用中的n不一樣於第2級調用中的n,所以程序建立了4個獨立的變量,雖然每一個變量的名字都是n,可是它們分別具備不一樣的值。當程序最終返回到對up_and_down()的第1級調用時,原來的n仍具備其初始值1.內存
第二,每一次函數調用都會有一次返回。當程序流執行到某一級遞歸的結尾處時,它會轉移到前1級遞歸繼續執行。程序不能直接返回到main()中初始調用部分,而是經過遞歸的每一級逐步返回,即從up_and_down()的某一級遞歸返回到調用它的那一級。
第三,遞歸函數中,位於遞歸調用前的語句和各級被調函數具備相同的執行順序。例如,在程序清單9.6中,打印語句#1位於遞歸調用語句以前。它按照遞歸調用的順序被執行了4次,即依次爲第1級、第2級、第3級、第4級。
第四,遞歸函數中,位於遞歸調用後的語句的執行順序和各個被調用函數的順序相反。例如,打印語句#2位於遞歸調用語句以後,其執行順序是第4級、第3級、第2級、第1級。遞歸調用的這種特性在解決涉及反向順序的編程問題時頗有用。下文中將給出這樣的一個例子。
第五,雖然每一級遞歸都有本身的變量,可是函數代碼並不會獲得複製。函數代碼是一系列計算機指令,而函數調用就是從頭執行這個指令集的下一條命令。一個遞歸調用會使程序從頭執行相應函數的指令集。除了爲每次調用建立變量,遞歸調用很是相似於一個循環語句。實際上,遞歸有時可被用來代替循環,反之亦然。
最後,遞歸函數中必須包含能夠終止遞歸調用的語句。一般狀況下,遞歸函數會使用一個if條件語句或其餘相似的語句以便當函數參數達到某個特定值時結束遞歸調用。好比在上例中,up_and_down(n)調用 了up_and_down(n+1).最後,實際參數的值達到4時,條件語句if(n<4)得不到知足,從而結束遞歸。
9.3.4 尾遞歸
最簡單的遞歸形式是把遞歸調用語句放在函數結尾即恰在return語句以前。這種形式被稱做尾遞歸(tail recursion)或結尾遞歸(end recursion),由於遞歸出如今函數尾部。因爲尾遞歸的做用至關於一條循環語句,因此它是最簡單的遞歸形式。
下面咱們講述分別使用循環和尾遞歸完成階乘計算的例子。一個整數的階乘就是從1到該數的乘積。例如,3的階乘(寫做3!)是1X2X3。0的階乘等於1,並且負數沒有階乘。程序清單9.7中,第一個函數使用for循環計算階乘,而第二個函數用的是遞歸方法。
程序清單 9.7 factor.c程序
//factor.c --使用循環和遞歸計算階乘 #include <stdio.h> long fact (int n); long rfact (int n); int main(void) { int num; printf("This program calculates factorials.\n"); printf("Enter a value in the range 0-12 (q to quit): \n"); while (scanf("%d",&num)==1) { if(num<0) printf("No negative numbers,please.\n"); else if (num>12) printf("Keep input under 13.\n"); else { printf("loop: %d factorial = %ld\n",num,fact(num)); printf("recursion: %d factorial = %ld\n",num,rfact(num)); } printf("Enter a value in the range 0-12 (q to quit): \n"); } printf("Bye.\n"); return 0; } long fact(int n) /*使用循環計算階乘*/ { long ans; for(ans=1;n>1;n--) ans*=n; return ans; } long rfact(int n) /*使用遞歸計算階乘*/ { long ans; if(n>0) ans=n*rfact(n-1); else ans=1; return ans; }
下面咱們研究使用遞歸方法的函數。其中關鍵一點是n!=n x (n-1)!。由於(n-1)!是1到n-1的全部正數之積,因此該數乘以n就是n的階乘。這也暗示了能夠採用遞歸的方法。調用rfact()時,rfact(n)就等於n x rfact(n-1)。這樣就能夠經過rfact(n-1)來計算rfact(n),如程序清單9.7中所示。固然 ,遞歸必須在某個地方結束,能夠在n爲0時把返回值設爲1,從而達到結束遞歸的目的。
在程序清單9.7中,兩個函數的輸出結果相同。雖然對rfact()的遞歸調用不是函數中的最後一行,但它是在n>0的狀況下執行的最後一條語句,所以也屬於尾遞歸。
既然循環和遞歸均可以用來實現函數,那麼究竟選擇哪個呢?通常來說,選擇循環更好一些。首先,由於每次遞歸調用都擁有本身的變量集合,因此就須要佔用較多的內存;每次遞歸調用須要把新的變量集合存儲在堆棧中。其次,因爲進行每次函數調用須要花費必定的時間,因此遞歸的執行速度較慢。既然如此,那麼咱們爲何還要講述以上例子呢?由於尾遞歸是最簡單的遞歸形式,比較容易理解;並且在某些時候,咱們不能使用簡單的循環語句代替遞歸,因此就有必要學習遞歸的方法。
9.3.4 遞歸和反向計算
下面咱們來考慮一個使用遞歸處理反序的問題(在這類問題中使用遞歸比使用循環更簡單)。
問題是這樣的,編寫一個函數將一個整數轉換成二進制形式。二進制的意思是指數值以2爲底數進行表示。
解決上述問題,須要使用一個算法(algorithm)。由於奇數的二進制形式的最後一位必定是1,而偶數的二進制數的最後一位是0,因此能夠經過5%2得出5的進制形式中最後一位數字是1或者是0。通常來說,對於數值n,其二進制數的最後一位是n%2,所以計算出的第一個數字剛好是須要輸出的最後一位。這就須要使用一個遞歸函數實現。在函數中,首先在遞歸調用以前計算n%2的數值,而後在遞歸調用語句以後進行輸出,這樣計算出的第一個數值反而在最後一個輸出。
爲了得出下一個數字,須要把原數值除以2。這種計算就至關於在十進制下把小數點左移一位。若是此時得出的數值是偶數,則下一個二進制數是0;若得出的數值是奇數,則下一個二進制數是1.例如,5/2的數值是2(整數除法),因此下一位值是0。這時已經獲得了數值01.重複以上計算,即便用2/2得出1,而1%2的數值是1,所以下一位數是1.這時獲得的數值是101.那麼什麼時候中止這種計算呢?由於只要被2除的結果大於或等於2,那麼就還須要一位二進制位進行表示,因此只有被2除的結果小於2時才中止計算。每次除以2就能夠得出一位二進制位值,直到計算出最後一位爲止。在程序清單9.8中實現以上算法:
程序清單9.8 binary.c程序
/*binary.c --以二進制形式輸出整數*/ #include <stdio.h> void to_binary(unsigned long n); int main(void) { unsigned long number; printf("Enter an integer (q to quit): \n"); while(scanf("%ul",&number)==1) { printf("Binary equivalent: "); to_binary(number); putchar('\n'); printf("Enter an integer (q to quit): \n"); } printf("Done.\n"); return 0; } void to_binary(unsigned long n)/*遞歸函數*/ { int r ; r = n%2; if(n>=2) to_binary(n/2); putchar('0'+r); /*以字符形式輸出*/ return 0; }
以上程序中,若是r 是0,表達式‘0’+r就是字符‘0’;當r爲1時,則該表達式的值爲字符‘1’。得出這種結果的前提假設是字符‘1’的數值編碼比字符‘0’的數值編碼大1.ASCII和EBCDIC兩種編碼都知足上述條件。更通常的方式,你可使用以下方法:
putchar(r ? '1' : '0' );
固然,不使用遞歸也能實現這個算法。可是因爲本算法先計算出最後一位的數值,因此在顯示結果以前必須對全部的數值進行存儲。
9.3.5 遞歸的優缺點
其優勢是在於爲某些編程問題提供了最簡單的方法,而缺點是一些遞歸算法會很快耗盡內存。同時,使用遞歸的程序難於閱讀和維護。從下面的例子,能夠看出遞歸的優缺點。
斐波納契數列定義以下:第一個和第二個數字都是1,然後續的每一個數字是前兩個數字之和。例如,數列中前幾個數字是1,1,2,3,5,8,13.下面咱們建立一個函數,它接受一個正整數n做爲參數,返回相應的斐波納契數值。
首先,關於遞歸深度,遞歸提供了一個簡單的定義。若是調用函數Fionacci(),當n爲1或2時Fabonacci(n)應返回1;對於其餘數值應返回Fibonacci(n-1)+Fabonacci(n-2) :
long Fabonacci(int n) { if(n>2) return Fibonacci(n-1)+Fibonacci(n-2); else return 1; }
這個C遞歸只是講述了遞歸的數學定義。同時本函數使用了雙重遞歸(double recursion);也就是說,函數對自己進行了兩次調用。這就會致使一個弱點。
爲了具體說明這個弱點,先假設調用函數Fibonacci(40)。第1級遞歸會建立變量n。接着它兩次調用Fibonacci(),在第2級遞歸中又建立兩個變量n。上述的兩次調用中的每一次又進行了再次調用,於是在第3級調用中須要4個變量n,這時變量總數爲7.由於每級調用須要的變量數是上級的兩倍,因此變量的個數是以指數規律增加的!這種狀況下,指數增加的變量數會佔用大量內存,這就可能致使程序癱瘓。固然,以上是一個比較極端的例子,但它也代表了必須當心使用遞歸,尤爲效率處於第一位時。
全部C函數地位同等(包括main()函數),每個函數均可以調用其餘任何函數或被其餘任何函數調用。