尾遞歸 是一種特殊的遞歸形式:遞歸調用發生在函數的最後一步操作,且其返回值直接作為當前函數的返回值,中間不再有任何額外計算。換句話說,函數在「尾部」完成遞歸,將計算狀態通過參數傳遞下去,不再需要保留當前棧幀。
編譯器若支援尾調用優化 (TCO, Tail Call Optimization),會將尾遞歸轉換為迭代跳轉 (JMP),消除棧幀增長,實現 O(1) 空間複雜度,效率與手寫迴圈相當。
| 版本 | 代碼片段 | 空間複雜度 | 備註 |
|---|---|---|---|
| 迭代 | for(i=1;i<=n;i++) r*=i; | O(1) | 基準方案 |
| 普通遞歸 | return n * fact(n-1); | O(n) | 需保留 n 個棧幀,乘法在回溯時執行 |
| 尾遞歸 | return fact_tail(n-1, acc*n); | O(1) (經 TCO) | 累加器 acc 攜帶中間結果,尾部直接返回 |
// 尾遞歸版階乘
long long fact_tail(int n, long long acc) {
if (n <= 1) return acc;
return fact_tail(n - 1, acc * n); // 尾調用:乘法在調用前完成,返回值直接透傳
}
// 調用入口
long long factorial(int n) { return fact_tail(n, 1); }
| 版本 | 時間複雜度 | 空間複雜度 | 關鍵點 |
|---|---|---|---|
| 迭代 | O(n) | O(1) | 雙變量滾動 |
| 普通遞歸 | O(2^n) | O(n) | 指數級重複計算 |
| 尾遞歸 | O(n) | O(1) (經 TCO) | 雙累加器 (a,b) 滾動傳遞 |
// 尾遞歸版斐波那契
long long fib_tail(int n, long long a, long long b) {
if (n == 0) return a;
if (n == 1) return b;
return fib_tail(n - 1, b, a + b); // 尾調用:狀態在參數中流轉
}
// 調用入口
long long fibonacci(int n) { return fib_tail(n, 0, 1); }
acc、斐波那契的 a,b)。因此,「尾遞歸 = 遞歸語法糖 + 迭代語義」。編譯器 TCO 將其還原為無條件跳轉 + 寄存器/棧槽更新,零棧增長。
| 編譯器 | 優化等級 | 支援情況 | 驗證方式 |
|---|---|---|---|
| GCC / Clang | -O1 / -O2 / -O3 / -Os | ✅ 完整支援 (含 C/C++/Objective-C) | objdump -d 檢查是否為 jmp 而非 call |
| MSVC (Visual C++) | /O2 / /Ox | ⚠️ 僅支援 x64 且需 __declspec(naked) 或特定模式;x86 通常不優化 | 查看反組裝碼 |
| JVM (Java) | -XX:+OptimizeStringConcat 等 | ❌ 不支援通用 TCO (安全模型限制) | 遞歸深度大會 StackOverflowError |
| V8 (Node.js/Chrome) | --harmony_tailcalls (已棄用) | ❌ ES6 標準雖規定但主流引擎未實現 | 嚴格模式下尾調用仍增長棧 |