【經典算法】遞歸解析


  在非負整數集上定義一個函數f,它滿足f(0)=0,且f(x)=2f(x-1)+x^2.從這個定義可以看出f(1)=1,f(2)=6,f(3)=21,f(4)=58。當一個函數用自身定義時就稱為遞歸(recursive).即,一個函數直接或間接地調用自身,是為直接或間接遞歸。C++是允許遞歸的。但必須記住,C++所做的僅僅是試圖遵循遞歸的思想。不是所有的數學遞歸函數都能有效的用C++遞歸模擬來實現。要點在於,遞歸函數f應該像非遞歸函數一樣只用幾行代碼就能表示出來。下圖給出了函數f的遞歸實現。

1 int f(int x) {
2     if (x == 0)
3         return 0;
4     else
5         return 2 * f(x - 1) + x * x;
6 }

第2行和第3行處理基准情況(base case),即此時函數的值可以直接算出來而不用遞歸。正如在沒有f(0)=0的前提下。聲稱f(x) = 2f(x - 1) + x^2.在數學上沒有意義一樣。C++的遞歸方法若無基准情況也是毫無意義的。第5行執行的是遞歸調用。

  編寫遞歸程序的時候,關鍵是要牢記遞歸的四條基本法則:

  1. 基准情形。 必須有某些基准情形不用遞歸就能求解。
  2. 不斷推進。 對於那些需要遞歸求解的情形。遞歸調用必須總能朝着基准情形的方向邁進。
  3. 設計法則。 假設所有的遞歸調用都能運行。
  4. 合成效益法則。 在求解一個問題的同一實例時,切勿在不同的遞歸調動中做重復性的工作。(攤還分析)

遞歸和循環

  如果我們要重復地多次計算相同的問題,通常可以選擇用遞歸或者循環兩種不同的方法。遞歸是在一個函數的內部調用這個函數自身。而循環這是通過設置計算的初始值及終止條件,在一個范圍內重復計算。比如求1+2+3+...+n,我們可以用遞歸或者循環兩種方式求出結果。對應的代碼如下:

  

int AddFrom1ToN_Recursive(int n) {
	return n <= 0 ? n + AddFrom1ToN_Recursive(n - 1);
}

int AddFrom1ToN_Iternative(int n) {
	int result = 0;
	for (int i = 0; i <= n; ++i)
		result += i;

	return result;

}

 

  通常遞歸的代碼比較簡潔。在上面的例子中,遞歸的代碼只有一個語句。而循環的則需要四個語句。在樹的前序、中序、后序遍歷算法的代碼中,遞歸的實現明顯要比循環簡單的多。

  面試小提示:

    通常基於遞歸的代碼要比基於循環實現的代碼要簡潔很多,更加容易實現。如果面試官沒有特殊要求,應聘者可以優先采用遞歸的方法編程。

 

  遞歸雖然有簡潔的優點,但它同時也有顯著的缺點。

  遞歸由於是函數調用自身,而函數調用是有空間和時間的消耗的:每一次函數調用,都需要在內存棧中分配空間以保存參數、返回的地址及臨時變量,而且往棧里壓入數據和彈出數據都需要時間。這就不難理解上述的例子中遞歸實現的效率不如循環。

  另外,遞歸中有可能很多計算都是重復的,從而對性能帶來很大的負面影響。遞歸的本質是把一個問題分解成兩個或多個小問題。如果多個小問題存在相互重疊的部分,那么就存在重復的計算。

  除了效率以外,遞歸還有可能引起更嚴重的問題:調用棧溢出。前面分析中提到需要為每一次函數調用在內存棧中分配空間,而每個進程的棧的容量是有限的。當遞歸調用的層級太多時,就會超出棧的容量,從而導致棧溢出。在上述的例子中,如果輸入的參數比較小,如10,它們都能返回結果55.但如果輸入的參數很大,比如5000,那么遞歸代碼在運行的時間就會出錯,但運行循環的代碼能得到正確的結果12502500.

 相關資料:

  1. http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=recursionPt1

  2. http://www.nowamagic.net/librarys/veda/detail/2314

參考資料:

  1. 《數據結構與算法分析C++描述》 Mark Allen Weiss

  2. 《劍指offer》 何海濤


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM