遞迴與尾遞迴 (C語言)

2021-07-09 06:32:48 字數 2845 閱讀 4749

在電腦科學領域中,遞迴式通過遞迴函式來實現的。程式呼叫自身的程式設計技巧稱為遞迴( recursion)。

乙個過程或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把乙個大型複雜的問題層層轉化為乙個與原問題相似的規模較小的問題來求解,遞迴策略只需少量的程式就可描述出解題過程所需要的多次重複計算,大大地減少了程式的**量。遞迴的能力在於用有限的語句來定義物件的無限集合。

一般來說,遞迴需要有:邊界條件、遞迴前進段和遞迴返回段。

當邊界條件不滿足時,遞迴前進;當邊界條件滿足時,遞迴返回。

注意:(1) 遞迴就是在過程或函式裡呼叫自身;

(2) 在使用遞迴策略時,必須有乙個明確的遞迴結束條件,稱為遞迴出口。

問題:計算n!

數學上的計算公式為:n!=n×(n-1)×(n-2)……2×1

使用遞迴的方式,可以定義為:

以遞迴的方式計算4!

f(4)=4×f(3)            遞迴階段

f(3)=3×f(2)

f(2)=2×f(1)

f(1)=1  終止條件

f(2)=(2)×(1)    回歸階段

f(3)=(3)×(2)

f(4)=(4)×(6)

24                 遞迴完成

以遞迴方式實現階乘函式的實現:

int fact(int n)
下面來詳細分析遞迴的工作原理

先看看c語言中函式的執行方式,需要了解一些關於c程式在記憶體中的組織方式:

堆的增長方向為從低位址到高位址向上增長,而棧的增長方向剛好相反(實際情況與cpu的體系結構有關)

當c程式中呼叫了乙個函式時,棧中會分配一塊空間來儲存與這個呼叫相關的資訊,每乙個呼叫都被當作是活躍的。棧上的那塊儲存空間稱為活躍記錄或者棧幀

棧幀由5個區域組成:輸入引數、返回值空間、計算表示式時用到的臨時儲存空間、函式呼叫時儲存的狀態資訊以及輸出引數,參見下圖:

可以使用下面的程式來檢驗:

#include 

int g1=0, g2=0, g3=0;

int max(int i)

int main(int argc, char **argv)

棧是用來儲存函式呼叫資訊的絕好方案,然而棧也有一些缺點:

棧維護了每個函式呼叫的資訊直到函式返回後才釋放,這需要占用相當大的空間,尤其是在程式中使用了許多的遞迴呼叫的情況下。除此之外,因為有大量的資訊需要儲存和恢復,因此生成和銷毀活躍記錄需要消耗一定的時間。我們需要考慮採用迭代的方案。幸運的是我們可以採用一種稱為尾遞迴的特殊遞迴方式來避免前面提到的這些缺點。

定義如果乙個函式中所有遞迴形式的呼叫都出現在函式的末尾,我們稱這個遞迴函式是尾遞迴的。當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。尾遞迴函式的特點是在回歸過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的**。

原理當編譯器檢測到乙個函式呼叫是尾遞迴的時候,它就覆蓋當前的活動記錄而不是在棧中去建立乙個新的。編譯器可以做到這點,因為遞迴呼叫是當前活躍期內最後一條待執行的語句,於是當這個呼叫返回時棧幀中並沒有其他事情可做,因此也就沒有儲存棧幀的必要了。通過覆蓋當前的棧幀而不是在其之上重新新增乙個,這樣所使用的棧空間就大大縮減了,這使得實際的執行效率會變得更高。雖然編譯器能夠優化尾遞迴造成的棧溢位問題,但是在程式設計中,我們還是應該盡量避免尾遞迴的出現,因為所有的尾遞迴都是可以用簡單的goto迴圈替代的。

例項為了理解尾遞迴是如何工作的,讓我們再次以遞迴的形式計算階乘。首先,這可以很容易讓我們理解為什麼之前所定義的遞迴不是尾遞迴。回憶之前對計算n!的定義:在每個活躍期計算n倍的(n-1)!的值,讓n=n-1並持續這個過程直到n=1為止。這種定義不是尾遞迴的,因為每個活躍期的返回值都依賴於用n乘以下乙個活躍期的返回值,因此每次呼叫產生的棧幀將不得不儲存在棧上直到下乙個子呼叫的返回值確定。現在讓我們考慮以尾遞迴的形式來定義計算n!的過程。

這種定義還需要接受第二個引數a,除此之外並沒有太大區別。a(初始化為1)維護遞迴層次的深度。這就讓我們避免了每次還需要將返回值再乘以n。然而,在每次遞迴呼叫中,令a=na並且n=n-1。繼續遞迴呼叫,直到n=1,這滿足結束條件,此時直接返回a即可。

**例項給出了乙個c函式facttail,它接受乙個整數n並以尾遞迴的形式計算n!。這個函式還接受乙個引數a,a的初始值為1。facttail使用a來維護遞迴層次的深度,除此之外它和fact很相似。讀者可以注意一下函式的具體實現和尾遞迴定義的相似之處。

int facttail(int n, int a)

示例中的函式是尾遞迴的,因為對facttail的單次遞迴呼叫是函式返回前最後執行的一條語句。在facttail中碰巧最後一條語句也是對facttail的呼叫,但這並不是必需的。換句話說,在遞迴呼叫之後還可以有其他的語句執行,只是它們只能在遞迴呼叫沒有執行時才可以執行。

尾遞迴是極其重要的,不用尾遞迴,函式的堆疊耗用難以估量,需要儲存很多中間函式的堆疊。比如f(n, sum) = f(n-1) + value(n) + sum; 會儲存n個函式呼叫堆疊,而使用尾遞迴f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後乙個函式堆疊即可,之前的可優化刪去。

也許在c語言中有很多的特例,但程式語言不只有c語言,在函式式語言erlang中(亦是棧語言),如果想要保持語言的高併發特性,就必須用尾遞迴來替代傳統的遞迴。

出處

遞迴與尾遞迴(C語言)

在電腦科學領域中,遞迴式通過遞迴函式來實現的。程式呼叫自身的程式設計技巧稱為遞迴 recursion 乙個過程或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把乙個大型複雜的問題層層轉化為乙個與原問題相似的規模較小的問題來求解,遞迴策略只需少量的程式就可描述出解題過程所需要的多次重複計算...

遞迴與尾遞迴

1 遞迴 關於遞迴的概念,我們都不陌生。簡單的來說遞迴就是乙個函式直接或間接地呼叫自身,是為直接或間接遞迴。一般來說,遞迴需要有邊界條件 遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進 當邊界條件滿足時,遞迴返回。用遞迴需要注意以下兩點 1 遞迴就是在過程或函式裡呼叫自身。2 在使用遞迴策略時...

遞迴與尾遞迴

1 遞迴 簡單的來說遞迴就是乙個函式直接或間接地呼叫自身,是為直接或間接遞迴。一般來說,遞迴需要有邊界條件 遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進 當邊界條件滿足時,遞迴返回。用遞迴需要注意以下兩點 1 遞迴就是在過程或函式裡呼叫自身。2 在使用遞迴策略時,必須有乙個明確的遞迴結束條件...