雖然你經常會看到下面的轉換因子,以尾調用的一個例子:
int factorial(int n, int acc=1) {
if (n <= 1) return acc;
else return factorial(n-1, n*acc);
}
它並不完全正確的,因爲它需要乘法既關聯和交換。 (乘法是關聯和交換,但上面並沒有作爲其不滿足這些約束等操作的模式。)一個更好的解決方案可能是:
int factorial(int n, int k=1, int acc=1) {
if (n == 0) return acc;
else return factorial(n-1, k+1, acc*k);
}
這也作爲一個模型爲斐波那契變換:
int fibonacci(int n, int a=1, int b=0) {
if (n == 0) return a;
else return fibonacci(n-1, a+b, a);
}
注意,這些計算的順序從頭開始,而不是在調用堆棧排隊待審的延續。所以它們在結構上比迭代解決方案更像迭代解決方案。與迭代程序不同,它們從不修改任何變量;所有綁定都是不變的。這是一個有趣和有用的屬性;在這些簡單的情況下,它沒有太大的區別,但是編寫代碼而不用重新分配會使編譯器優化變得更容易。
無論如何,最後一個確實爲您的遞歸函數提供了一個模型;像斐波那契序列,我們需要保持過去相關的價值觀,但我們需要他們三個而不是兩個:
int mouse(int n, int a=1, int b=1, int c=1) {
if (n <=2) return a;
else return mouse(n-1, a*c+1, a, b);
}
附錄
在評論,兩個問題提出了。我會盡量在這裏回答他們(還有一個)。首先,應該清楚(從考慮沒有函數調用概念的底層機器結構),任何函數調用都可以改寫爲goto(可能帶有非有界中間存儲器)。此外,任何goto都可以表示爲tail-call。所以有可能(但不一定非常漂亮)將任何遞歸重寫爲尾遞歸。
通常的機制是「延續傳遞風格」,這是一種奇特的說法,每次你想調用一個函數時,你將當前函數的其餘部分打包爲新函數(「延續」) ,並將該延續傳遞給被調用的函數。由於每個函數都接收到一個延續作爲參數,所以它必須通過調用它所接收的延續來完成它創建的任何延續。
這可能足以讓你的頭腦旋轉,所以我會換一種方式:不是將參數和返回位置推入堆棧並調用函數(稍後返回),而是推入參數和延續定位到堆棧並轉到一個函數,該函數稍後會轉到繼續位置。簡而言之,你只需將堆棧作爲一個明確的參數,然後你永遠不需要返回。這種編程風格在事件驅動代碼中很常見(請參閱Python Twisted),並且寫入(和讀取)是一種真正的痛苦。所以我強烈建議讓編譯器爲你做這個轉換,如果你能找到一個能做到這一點的轉換。
@xxmouse建議我將遞歸方程從帽子中拉出來,並詢問它是如何派生的。它只是原來的遞歸,但重新作爲一個元組的改造:
fn = fn-1*fn-3 + 1
=>
Fn = <Fn-11*Fn-13+1, Fn-11, Fn-12>
我不知道如果這是再清楚不過,但它是我能做到的最好。看看斐波納契的例子,稍微簡單些。
@j_random_hacker問這個轉換的限制是什麼。它適用於遞歸序列,其中每個元素可以用前面k
元素的某個公式表示,其中k
是常數。還有其他方法可以產生尾部回叫遞歸。例如:上述
// For didactic purposes only
bool is_odd(int n) { return n%2 == 1; }
int power(int x, int n, int acc=1) {
if (n == 0) return acc;
else if (is_odd(n)) return power(x, n-1, acc*x);
else return power(x*x, n/2, acc);
}
是不一樣的通常非尾部調用遞歸,它不乘法的不同(但等效的和等長的)序列。
int squared(n) { return n * n; }
int power(int x, int n) {
if (n == 0) return 1;
else if (is_odd(n)) return x * power(x, n-1));
else return squared(power(x, n/2));
}
感謝阿列克謝伏龍以下測試: 輸出(ideone):
mouse(0) = 1
mouse(1) = 1
mouse(2) = 1
mouse(3) = 2
mouse(4) = 3
mouse(5) = 4
mouse(6) = 9
mouse(7) = 28
mouse(8) = 113
mouse(9) = 1018
真正的尾遞歸需要你要麼自己處理堆棧,它在裝配寫,或有編譯器支持。我不相信Perl目前支持它。 – Dai 2013-03-21 00:41:04
我覺得@Dai你對尾部遞歸_removal_,這是一個編譯器優化感到困惑。我想,他只是要求轉換爲尾遞歸函數。 – Gene 2013-03-21 00:49:48