因為在樹的內容中,要大量遇到遞迴的操作,所以著這裡複習一下遞迴操作。
遞迴,就是自己調用自己。
首先,需要搞清楚函數是如何調用的。在執行被調函數之前,系統需要做3件事:
1.將實參,函數的返回地址等資訊傳遞給被調函數儲存。
2.為被掉函數的形參,局部變數分配空間
3.將控制轉移到被調函數入口。
當從被調函數返回前,也需要做3件事:
1.儲存函數的返回結果
2.釋放被調函數儲存空間
3.按照被調函數儲存的返回地址將控制轉移給調用函數。
其中主調函數和被調函數間的參數傳遞和控制轉移是通過棧來實現的:電腦只能操作棧頂元素。當調用被掉函數時,將被調函數的資料空間壓棧,則即將控制權轉移給了被掉函數;當函數結束時,被調函數出棧,則控制權又返回給了主調函數。
從此可以看出,電腦程式調用別的程式和調用自己是沒有區別的,同樣都是做上述事情。
讓我們從一個簡單的遞迴問題開始吧,求1到100的和。
//求和int sum(int n){if(0 == n)return 0;elsereturn n+sum(n-1);}
它的求解思路是這樣的:雖然我不知道0+……+100的和是多少,但是我知道0+……+100的和肯定等於0+……+99的和再加上100;那麼0+……+99的和又等於多少呢,我不知道,但是我知道他肯定等於0+……+98的和再加上99;依次這樣類推,總會有:那麼0自己的和是多少呢(這樣說很坑爹),答案是0,所以能從0自己的和推出0+1的和,推出0+1+2的和,此次就能計算出0+……+100的和了。
與一般的迭代相比遞迴有這麼幾個特點:
1.遞迴更貼近於人的“逆向思維”,而迭代卻是不動腦子的直接做。
2.從程式的複雜度上來說,遞迴比較短小,而迭代會比較長。
3.從執行效率來講,遞迴效率低(因為函數調用要做許多事),而迭代效率較高。
4.當程式中有bug時,迭代的錯誤往往容易發現,而遞迴的錯誤找起來卻很費力。
到底什麼時候用遞迴,什麼時候用迭代的呢?如果你想少用一點自己腦子,而讓電腦多累一點,那麼就用遞迴;如果你想讓自己考慮清楚所有的步驟,電腦直接照著執行,那麼就用迭代。
其實,並不是所有的問題都能轉化成遞迴問題的,當使用遞迴分析問題時,必須保證遞迴以後,問題的本質沒有發生變化,但是規模減小了,而且當規模小到一定程度時,我能求解他。
下面再看幾個經典的遞迴程式的例子:
//求階乘int factorial(int n){if (n == 1)return 1;elsereturn n*factorial(n-1);}//Fibonacci數列long Fibonacci(int n){if(n == 0)return 0;else if(n == 1)return 1;elsereturn Fibonacci(n-1)+Fibonacci(n-2);}//漢諾塔void hanNuoTa(int n,char A,char B,char C){if( 1 == n )printf("move disk %d from %c to %c\n",n,A,C);else{hanNuoTa(n-1,A,C,B);printf("move disk %d from %c to %c\n",n,A,C);hanNuoTa(n-1,B,A,C);}}
尤其是漢諾塔,非常值得玩味。但是由於說起來很囉唆,這裡就不提了。
最後送一句名言給大家:To iterate is human,to recurse divine.—— L. Peter Deutsch。這句話非常值得玩味,到底是說迭代是人,遞迴是神,還是想說一般人你就迭代做吧,如果你是高手,那麼再用遞迴,恐怕沒人能說得清。