遞迴在解決某些問題的時候使得我們思考的方式得以簡化,**也更加精煉,容易閱讀。那麼既然遞迴有這麼多的優點,我們是不是什麼問題都要用遞迴來解決呢?難道遞迴就沒有缺點嗎?今天我們就來討論一下遞迴的不足之處。談到遞迴就不得不面對它的效率問題。
為什麼遞迴是低效的
還是拿斐波那契(fibonacci)數列來做例子。在很多教科書或文章中涉及到遞迴或計算複雜性的地方都會將計算斐波那契數列的程式作為經典示例。如果現在讓你以最快的速度用c#寫出乙個計算斐波那契數列第n個數的函式(不考慮引數小於1或結果溢位等異常情況),我不知你的程式是否會和下列**類似:
public static ulong fib(ulong n)
return (n == 1 || n == 2) ? 1 : fib(n - 1) + fib(n - 2);
這段**應該算是短小精悍(執行**只有一行),直觀清晰,而且非常符合許多程式設計師的**美學,許多人在面試時寫出這樣的**可能心裡還會暗爽。但是如果用這段**試試計算fib(1000)我想就再也爽不起來了,它的執行時間也許會讓你抓狂。
看來好看的**未必中用,如果程式在效率不能接受那美觀神馬的就都是浮雲了。如果簡單分析一下程式的執行流,就會發現問題在哪,以計算fibonacci(5)為例:
從上圖可以看出,在計算fib(5)的過程中,fib(1)計算了兩次、fib(2)計算了3次,fib(3)計算了兩次,本來只需要5次計算就可以完成的任務卻計算了9次。這個問題隨著規模的增加會愈發凸顯,以至於fib(1000)已經無法再可接受的時間內算出。
我們當時使用的是簡單的用定義來求 fib(n),也就是使用公式 fib(n) = fib(n-1) + fib(n-2)。這樣的想法是很容易想到的,可是仔細分析一下我們發現,當呼叫fib(n-1)的時候,還要呼叫fib(n-2),也就是說fib(n-2)呼叫了兩次,同樣的道理,呼叫f(n-2)時f(n-3)也呼叫了兩次,而這些冗餘的呼叫是完全沒有必要的。可以計算這個演算法的複雜度是指數級的。
改進的斐波那契遞迴演算法
那麼計算斐波那契數列是否有更好的遞迴演算法呢? 當然有。讓我們來觀察一下斐波那契數列的前幾項:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55 …
注意到沒有,如果我們去掉前面一項,得到的數列依然滿足f(n) = f(n-1) – f(n-2), (n>2),而我們得到的數列是以1,2開頭的。很容易發現這個數列的第n-1項就是原數列的第n項。怎麼樣,知道我們該怎麼設計演算法了吧?我們可以寫這樣的乙個函式,它接受三個引數,前兩個是數列的開頭兩項,第三個是我們想求的以前兩個引數開頭的數列的第幾項。
int fib_i(int a, int b, int n);
在函式內部我們先檢查n的值,如果n為3則我們只需返回a+b即可,這是簡單情境。如果n>3,那麼我們就呼叫f(b, a+b, n-1),這樣我們就縮小了問題的規模(從求第n項變成求第n-1項)。好了,最終**如下:
int fib_i(int a, int b , int n)
if(n == 3)
return a+b;
else
return fib_i(b, a+b, n-1);
這樣得到的演算法複雜度是o(n)的。已經是線性的了。它的效率已經可以與迭代演算法的效率相比了,但由於還是要反覆的進行函式呼叫,還是不夠經濟。
遞迴與迭代的效率比較
我們知道,遞迴呼叫實際上是函式自己在呼叫自己,而函式的呼叫開銷是很大的,系統要為每次函式呼叫分配儲存空間,並將呼叫點壓棧予以記錄。而在函式呼叫結束後,還要釋放空間,彈棧恢復斷點。所以說,函式呼叫不僅浪費空間,還浪費時間。
這樣,我們發現,同乙個問題,如果遞迴解決方案的複雜度不明顯優於其它解決方案的話,那麼使用遞迴是不划算的。因為它的很多時間浪費在對函式呼叫的處理上。在c++中引入了內聯函式的概念,其實就是為了避免簡單函式內部語句的執行時間小於函式呼叫的時間而造成效率降低的情況出現。在這裡也是乙個道理,如果過多的時間用於了函式呼叫的處理,那麼效率顯然高不起來。
舉例來說,對於求階乘的函式來說,其迭代演算法的時間複雜度為o(n):
int fact(n)
int i;
int r = 1;
for(i = 1; i < = n; i++)
r *= i;
return r;
而其遞迴函式的時間複雜度也是o(n):
int fact_r(n)
if(n == 0)
return 1;
else
return n * f(n);
但是遞迴演算法要進行n次函式呼叫,而迭代演算法則只需要進行n次迭代而已。其效率上的差異是很顯著的。
小結由以上分析我們可以看到,遞迴在處理問題時要反覆呼叫函式,這增大了它的空間和時間開銷,所以在使用迭代可以很容易解決的問題中,使用遞迴雖然可以簡化思維過程,但效率上並不合算。效率和開銷問題是遞迴最大的缺點。
雖然有這樣的缺點,但是遞迴的力量仍然是巨大而不可忽視的,因為有些問題使用迭代演算法是很難甚至無法解決的(比如漢諾塔問題)。這時遞迴的作用就顯示出來了。
遞迴的效率問題暫時討論到這裡。後面會介紹到遞迴計算過程與迭代計算過程,講解得更詳細點。
延伸閱讀
漫談遞迴 遞迴的思想
為什麼要用遞迴 程式設計裡面估計最讓人摸不著頭腦的基本演算法就是遞迴了。很多時候我們看明白乙個複雜的遞迴都有點費時間,尤其對模型所描述的問題概念不清的時候,想要自己設計乙個遞迴那麼就更是有難度了。用歸納法來理解遞迴 數學都不差的我們,第一反應就是遞迴在數學上的模型是什麼。畢竟我們對於問題進行數學建模...
用循壞代替遞迴效能更佳
用10階乘的結果 3628800,比較兩者的耗時效能 遞迴用時 23ms 迴圈用時 0ms 忽略不計 結論 用循壞代替遞迴效能更佳 package annotation public class test01 遞迴效能不好 param num return static int factorial ...
2 3 5 遞迴規則與文法的遞迴性
1.遞迴規則 所謂遞迴規則,是指在規則的左部和右部具有相同非終結符的規則。如果文法中有規則 a a 稱為規則左遞迴。如果文法中有規則 a a 稱為規則右遞迴。如果文法中有規則 a a 稱為規則遞迴。2.文法的遞迴性 文法的遞迴性是指對文法中任一非終結符,若能建立乙個推導過程,在推導所得的符號串 中又...