為什麼用 遞迴 計算“階乘”和“斐波那契數列”是不合適的?
我們看到的參考書中,當講到遞迴時基本上都是使用“階乘”和“斐波那契數列”來舉例子,確實可以幫助我們瞭解遞迴,但卻導致我們在平時編寫類似程式時,總是使用遞迴來實現。那麼在實際專案中,使用遞迴來實現這兩個程式到底是否合適?答案是否定的。
《C和指標》的作者Kenneth A. Reek說,他認為這是很不幸的:“計算階乘時不能提供任何優越之處;在斐波那契數列中,使用遞迴效率非常非常低”。 尤其對於計算斐波那契數,使用遞迴和使用迭代的效率有可能相差幾十萬倍,下面有程式碼分析。
1. 技巧
1.1 “尾部遞迴”可以使用“迭代”來替換。 學過資料結構之後,對這個應該不陌生。
1.2 遞迴的優點在於易於理解;迭代的優點在於效率高。如何選擇取決於你在“易於理解”和“效率”中如何作出權衡。
遞迴函式呼叫涉及一些執行時開銷,包括引數必須壓到堆疊中、為區域性變數分配記憶體、暫存器的值必須儲存等等,並且在每次呼叫返回時,上述儲存操作必須還原,恢復成呼叫前的樣子。迭代的實現方式開銷顯然要小。所以在可理解性相差不大的情況下,為了保證效率應該優先選擇迭代。
2. 階乘
階乘的一種定義方式為:
F(n) = 1 當 n<=0 F(n) = n * F(n-1) 當n>0 |
2.1 遞迴實現
實現程式碼如下:
/* 利用 遞迴 來計算整數n的階乘 */
long factorial_recursion( int n ){
if( n<=0 ){
return 1;
}else{
return n*factorial_recutsion( n-1 );
}
}
很明顯是“尾部遞迴”,所以我們可以很輕鬆的改寫為“迭代”的形式。
2.2 迭代實現
迭代實現程式碼如下:
/* 利用 迭代 來計算整數n的階乘 */ long factorial_iteration( int n ){ int result = 1; while( n>1 ){ result *=n; n--; } return result; }
2.3 兩種方式的比較
很明顯,兩種實現方式都非常簡單易懂,在實際專案中,為了效率,應該優先選擇 迭代。
3. 斐波那契數列
斐波那契數列中的每個數,都是它前面兩個數的和。其計算公式如下:
F(0) = 0 F(1) = 1 F(n)= F(n-1) + F(n-2) |
3.1 遞迴實現
遞迴實現程式碼如下:
/* 計算斐波那契數列的第num個數 */
int fibonacci_count( int num ){
if( num<=2 ){
return 1;
}else{
return fibonacci_count(num-1) + fibonacci_count(num-2);
}
}
3.2 迭代實現
迭代實現程式碼如下:
/*
利用 迭代 計算 斐波那契數
其形式為: 1, 1, 2, 3, 5, 8, 13, 21, 34 …
*/
long fibonacci(int n){
long result;
long previous;
long next;
result = previous = 1;
if(n<=2){
return 1;
}
int i =3; /* 用於計算已經計算到第幾個 */
while( i<=n ){
next = result;
result = previous + next;
previous = next;
i++;
}
return result;
}
3.3 兩種方式比較
下面一個程式反映了,利用遞迴方式計算斐波那契函式時,呼叫fibonacci函式的次數,以及計算fibonacci(3)的次數。
#include <stdio.h>
#include <stdlib.h>
int fibonacci_count( int num );
long count; /* 用來記錄斐波那契數列中計算呼叫 fibonacci_rec_count函式 的次數 */
int num_three; /* 計算 fibonacci_rec_count(3)的次數 */
int main(){
printf("Fibonacci(num)/t/tNumber of Calls/t/tnumbers of Call fibonacci_rec_count(3) /n");
int i;
for( i=1; i<=30; i++ ){
int result = fibonacci_rec_count(i);
printf("num: %d/t/t/tcount: %ld /t/t%d/n", i, count, num_three);
count = 0;
num_three = 0;
}
getchar();
return EXIT_SUCCESS;
}
/* 計算斐波那契數列的第num個數 */
int fibonacci_rec_count( int num ){
count++; /* 每呼叫一次fibonacci_rec_count函式,count的值便加1 */
if( num==3 ){
num_three++;
}
if( num<=2 ){
return 1;
}else{
return fibonacci_rec_count(num-1) + fibonacci_rec_count(num-2);
}
}
執行結果為:
Fibonacci(num) Number of Calls numbers of fibonacci(3)
num: 1 count: 1 0
num: 2 count: 1 0
num: 3 count: 3 1
num: 4 count: 5 1
num: 5 count: 9 2
num: 6 count: 15 3
num: 7 count: 25 5
num: 8 count: 41 8
num: 9 count: 67 13
num: 10 count: 109 21
num: 11 count: 177 34
num: 12 count: 287 55
num: 13 count: 465 89
num: 14 count: 753 144
num: 15 count: 1219 233
num: 16 count: 1973 377
num: 17 count: 3193 610
num: 18 count: 5167 987
num: 19 count: 8361 1597
num: 20 count: 13529 2584
num: 21 count: 21891 4181
num: 22 count: 35421 6765
num: 23 count: 57313 10946
num: 24 count: 92735 17711
num: 25 count: 150049 28657
num: 26 count: 242785 46368
num: 27 count: 392835 75025
num: 28 count: 635621 121393
num: 29 count: 1028457 196418
num: 30 count: 1664079 317811
在遞迴實現方式中,每次遞迴呼叫都會觸發另外兩個遞迴呼叫,而且這兩個呼叫的任何一個又會觸發兩個遞迴呼叫,依次越來越多。冗餘的計算增長迅速。例如:在計算fibonacci(10) 時,fibonacci(3)被計算了21次;但在計算fibonacci(30) 時,fibonacci(3)被計算了 317811 次;這 317811 次計算結果完全一樣,除了一次有用外,其餘都是浪費。
參考資料:《C和指標》