【經典算法】遞歸解析

  在非負整數集上定義一個函數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;

}

 

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

  面試小提示:spa

    一般基於遞歸的代碼要比基於循環實現的代碼要簡潔不少,更加容易實現。若是面試官沒有特殊要求,應聘者能夠優先採用遞歸的方法編程。.net

 

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

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

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

  除了效率之外,遞歸還有可能引發更嚴重的問題:調用棧溢出。前面分析中提到須要爲每一次函數調用在內存棧中分配空間,而每一個進程的棧的容量是有限的。當遞歸調用的層級太多時,就會超出棧的容量,從而致使棧溢出。在上述的例子中,若是輸入的參數比較小,如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》 何海濤

相關文章
相關標籤/搜索