遞歸就是不斷調用自身,就是A調用A的過程。遞歸會不斷使用棧來存過程,佔用的空間很大。很容易造成堆棧溢出的情況,改進可以使用尾遞歸的形式。遞歸有兩個過程:
1)遞推;
2)迴歸。
1
2
3
4
5
6
7
|
int sum1( int n) { if (n
== 0) return 0; else return n+sum1(n-1); } |
迭代是A不斷的調用B的過程,使用一個循環語句的形式。
1
2
3
4
5
6
7
|
int sum2( int n) { int ret
= 0 ; for ( int i
= 1 ;
i <= n; i++) ret
+= i; return ret; } |
計算的過程是:
0+1=11+2=3
3+3=6
6+4=10
10+5=15
尾遞歸:
一個遞歸函數,它的計算過程是迭代的,就是尾遞歸函數,記住遞歸調用在遞歸實例中恰好以最後一步操作的形式出現。
Here's a tail-recursive version of the same function:
int tailrecsum(x, running_total=0):
if x == 0:
return running_total
else:
return tailrecsum(x - 1, running_total + x)
Here's the sequence of events that would occur if you called tailrecsum(5)
,
(which would effectively be tailrecsum(5,
0)
, because of the default second argument).
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
這是尾遞歸:
(這個程序沒什麼意義,僅作爲理解輔助之用)。
這不是尾遞歸:
後者不是尾遞歸,是因爲該函數的最後一步操作是用1加上f(x-1)的返回結果,因此,最後一步操作不是調用自身。注意,後面這段代碼的遞歸也發生在函數末尾,但它不是尾遞歸。尾遞歸的判斷標準是函數運行最後一步是否調用自身,而不是是否在函數的最後一行調用自身。因此階乘n!的遞歸求法,儘管看起來遞歸發生在函數末尾,其實也不是尾遞歸:
使用尾遞歸可以帶來一個好處:因爲進入最後一步後不再需要參考外層函數(caller)的信息,因此沒必要保存外層函數的stack,遞歸需要用的stack只有目前這層函數的,因此避免了棧溢出風險。
一些語言提供了尾遞歸優化,當識別出使用了尾遞歸時,會相應地只保留當前層函數的stack,節省內存。
所以,在沒有尾遞歸優化的語言,如java, python中,鼓勵用迭代iteration來改寫尾遞歸;在有尾遞歸優化的語言如Erlang中,鼓勵用尾遞歸來改寫其他形式的遞歸。
function f(x) {
if (x === 1) return 1;
return f(x-1);
}
(這個程序沒什麼意義,僅作爲理解輔助之用)。
這不是尾遞歸:
function f(x) {
if (x === 1) return 1;
return 1 + f(x-1);
}
後者不是尾遞歸,是因爲該函數的最後一步操作是用1加上f(x-1)的返回結果,因此,最後一步操作不是調用自身。注意,後面這段代碼的遞歸也發生在函數末尾,但它不是尾遞歸。尾遞歸的判斷標準是函數運行最後一步是否調用自身,而不是是否在函數的最後一行調用自身。因此階乘n!的遞歸求法,儘管看起來遞歸發生在函數末尾,其實也不是尾遞歸:
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n-1); // 最後一步不是調用自身,因此不是尾遞歸
}
使用尾遞歸可以帶來一個好處:因爲進入最後一步後不再需要參考外層函數(caller)的信息,因此沒必要保存外層函數的stack,遞歸需要用的stack只有目前這層函數的,因此避免了棧溢出風險。
一些語言提供了尾遞歸優化,當識別出使用了尾遞歸時,會相應地只保留當前層函數的stack,節省內存。
所以,在沒有尾遞歸優化的語言,如java, python中,鼓勵用迭代iteration來改寫尾遞歸;在有尾遞歸優化的語言如Erlang中,鼓勵用尾遞歸來改寫其他形式的遞歸。