遞歸和尾遞歸的區別和實現

基本上大多數C的入門教材裏都會說簡單的遞歸,例如求階乘n!,經典的本科入門書籍譚浩強的《C語言程序設計》,但後來看了《代碼大全2》這本書,關於進階和編碼規範的書中提到了,這些計算機教材用愚蠢的例子階乘和斐波那契數列來說解階乘,由於遞歸是強有力的工具,但用階乘去計算階乘之類的,很不明智,除了速度慢,還沒法預測運行期間內存的使用狀況,並且遞歸比循環更難理解。該書說的這些點,的確是遞歸的一個弊病,但有些時候,遞歸的思想很不錯,二叉樹的不少遍歷問題,或者排序算法,用遞歸實現起來很方便。遞歸的代碼比較簡潔,但使用起來要慎重,有如上所說的一些弊端,但遞歸的思想是解決一些能夠迭代的問題的。算法

1. 遞歸 
 遞歸的名次解釋(百度百科的): 編程

     程序調用自身的編程技巧稱爲遞歸( recursion)。遞歸作爲一種算法在程序設計語言中普遍應用。一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它一般把一個大型複雜的問題層層轉化爲一個與原問題類似的規模較小的問題來求解,遞歸策略只需少許的程序就可描述出解題過程所須要的屢次重複計算,大大地減小了程序的代碼量。遞歸的能力在於用有限的語句來定義對象的無限集合。通常來講,遞歸須要有邊界條件、遞歸前進段和遞歸返回段。當邊界條件不知足時,遞歸前進;當邊界條件知足時,遞歸返回。編程語言

 

定義以下:函數式編程

遞歸,就是在運行的過程當中調用本身。函數

構成遞歸需具有的條件:工具

1. 子問題須與原始問題爲一樣的事,且更爲簡單;oop

2. 不能無限制地調用自己,須有個出口,化簡爲非遞歸情況處理。優化

 

以遞歸方式實現階乘函數的實現:編碼

 

int fact(int n) {
if (n < 0)
return 0;
else if(n == 0 || n == 1)
return 1;
else
return n * fact(n - 1);
}
 .net

 

 

 

 

再好比求二叉樹的高度就是1+max{height(root->light),height(root->right)},從而有了遞歸算法的求解思路。

遞歸實現的代碼以下:

 

int height(BTree *p)
{
int hi = 0,lh = 0,rh = 0;
if (p == NULL)
hi = 0;
else
{
if (p->lchild ==NULL)
lh = 0;
else
lh = height(p->lchild);//遞歸求解左子樹的高度
if (p->rchild ==NULL)
rh = 0;
else
rh = height(p->rchild);//遞歸求解右子樹的高度
hi = lh>rh ? (lh + 1) : (rh + 1);
}
return hi;
}
 

 

 

下面分析遞歸的工做原理:

先看看C程序在內存中的組織方式:  http://blog.csdn.net/zcyzsy/article/details/69788884

(個人這篇博文有詳細寫,這裏不作複述):

BSS段,數據段 ,代碼段,堆(heap),棧(stack) ;

  而棧又稱堆棧,存放程序的局部變量(不包括靜態局部變量,static變量存在靜態區)。除此之外,在函數被調用時,棧用來傳遞參數和返回值。因爲棧的後進先出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。

    當C程序中調用了一個函數時,棧中會分配一塊空間來保存與這個調用相關的信息,每個調用都被看成是活躍的。棧上的那塊存儲空間稱爲活躍記錄或者棧幀

    棧幀由5個區域組成:輸入參數、返回值空間、計算表達式時用到的臨時存儲空間、函數調用時保存的狀態信息以及輸出參數。

    棧是用來存儲函數調用信息的絕好方案,然而棧也有一些缺點:

    棧維護了每一個函數調用的信息直到函數返回後才釋放,這須要佔用至關大的空間,尤爲是在程序中使用了許多的遞歸調用的狀況下。除此以外,由於有大量的信息須要保存和恢復,所以生成和銷燬活躍記錄須要消耗必定的時間。咱們須要考慮採用迭代的方案。

簡而言之,遞歸過的壓棧和出棧,時間和空間都有很大的消耗,

 

2.尾遞歸 
   幸虧能夠採用一種稱爲尾遞歸的特殊遞歸方式來避免前面提到的這些缺點。

尾遞歸的名次解釋(百科來的,供理解):

     若是一個函數中全部遞歸形式的調用都出如今函數的末尾,咱們稱這個遞歸函數是尾遞歸的。當遞歸調用是整個函數體中最後執行的語句且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。尾遞歸函數的特色是在迴歸過程當中不用作任何操做,這個特性很重要,由於大多數現代的編譯器會利用這種特色自動生成優化的代碼。

 

尾遞歸的原理:

     當編譯器檢測到一個函數調用是尾遞歸的時候,它就覆蓋當前的活動記錄而不是在棧中去建立一個新的。編譯器能夠作到這點,由於遞歸調用是當前活躍期內最後一條待執行的語句,因而當這個調用返回時棧幀中並無其餘事情可作,所以也就沒有保存棧幀的必要了。經過覆蓋當前的棧幀而不是在其之上從新添加一個,這樣所使用的棧空間就大大縮減了,這使得實際的運行效率會變得更高。

 

以尾遞歸方式實現階乘函數的實現:

 

int facttail(int n, int res)
{
if (n < 0)
return 0;
else if(n == 0)
return 1;
else if(n == 1)
return res;
else
return facttail(n - 1, n *res);
}
 

 

 

 

那麼尾遞歸是如何工做的,咱們先用遞歸來計算階乘,經過對比,看看前面所定義的遞歸爲什麼不是尾遞歸。

代碼1:在每次函數調用計算n倍的(n-1)!的值,讓n=n-1並持續這個過程直到n=1爲止。這種定義不是尾遞歸的,由於每次函數調用的返回值都依賴於用n乘如下一次函數調用的返回值,所以每次調用產生的棧幀將不得不保存在棧上直到下一個子調用的返回值肯定。

代碼2:函數比代碼1多個參數res,除此以外並無太大區別。res(初始化爲1)維護遞歸層次的深度。這就讓咱們避免了每次還須要將返回值再乘以n。然而,在每次遞歸調用中,令res=n*res而且n=n-1。繼續遞歸調用,直到n=1,這知足結束條件,此時直接返回res便可。

能夠仔細看看兩個函數的具體實現,看看遞歸和尾遞歸的不一樣!

示例中的函數是尾遞歸的,由於對facttail的單次遞歸調用是函數返回前最後執行的一條語句。換句話說,在遞歸調用以後還能夠有其餘的語句執行,只是它們只能在遞歸調用沒有執行時才能夠執行。

尾遞歸是極其重要的,不用尾遞歸,函數的堆棧耗用難以估量,須要保存不少中間函數的堆棧。好比sum(n) = f(n) = f(n-1) + value(n) ;會保存n個函數調用堆棧,而使用尾遞歸f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後一個函數堆棧便可,以前的可優化刪去。

 

 

關於尾遞歸,解釋下,其實一開始接觸尾遞歸是實習期間,用erlang函數式編程語言寫項目程序,erlang中沒有循環,只能經過遞歸和列表解析來實現循環的功能。但衆所周知遞歸是代碼好看但效率極低的,因而我看到了erlang的編程之美-尾遞歸。當時帶個人大哥只給了一句,不壓棧的遞歸,放心用,我也實際測了時間,效率之高使人驚豔。在erlang裏很好的體驗了一波尾遞歸的強大,多變,實用,靈活。當時沒去想C的尾遞歸,如今以爲要總結一波,C中固然也有高效的尾遞歸啦。

 

下面貼一段之前寫erlang時的遞歸和尾遞歸代碼:

 

% 遞歸
loop(0)->
1;
loop(N)->
N * loop(N - 1).

% 尾遞歸
tail_loop(N)->
tail_loop(N, 1).

tail_loop(0,R)->
R;
tail_loop(N,R) ->
tail_loop(N - 1, N*R).
 

 

 

 

貼幾段:
%%尾遞歸實現循環和判斷,列表解析實現列表元素的遍歷,
%%短小精悍,實際上是查找列表中是否有連續3個相同的數(和傳的參相同)
get_aj([],H)->false;
get_aj([H|[H|[H|D]]],H)->true;
get_aj([_|D],H)->get_aj(D,H).
 

 

  注意哦,這裏尾遞歸其實還用了一個重載,而後尾遞歸調用,其實最精髓就是 經過參數傳遞結果,達到不壓棧的目的。C中玩好了尾遞歸,代碼能夠很秀。尾遞歸是一種高效解決問題的思想,C和erlang中的尾遞歸都是同樣的。原理相同,效果相同。--------------------- 做者:Zmyths 來源:CSDN 原文:https://blog.csdn.net/zcyzsy/article/details/77151709 版權聲明:本文爲博主原創文章,轉載請附上博文連接!

相關文章
相關標籤/搜索