遞歸是指函數直接或間接調用自身的編程技術。一個遞歸函數必須包含兩個核心要素:基線條件(終止遞歸的條件)和遞歸步驟(將問題分解為更小的同類子問題)。
經典例子——十進制轉二進制輸出:
void binary_to_ascii(unsigned int value) {
unsigned int quotient = value / 10;
if (quotient != 0)
binary_to_ascii(quotient); // 遞歸調用
putchar(value % 10 + '0'); // 輸出當前位
}
這裡利用遞歸的「後進先出」特性,實現了從高位到低位的正序輸出——這是遞歸獨特優勢之一。
以「5人坐成一排,右邊比左邊大2歲,最左10歲,問最右年齡」為例:
int age(int n) {
if (n == 1) return 10;
return age(n - 1) + 2;
}
調用age(5)時,系統依次調用age(4)、age(3)...age(1),這如同發生嵌套中斷:每層調用前保護現場(參數、返回地址等入棧),到達基線條件後逐層返回(恢復現場、出棧計算)。這完美對應微機原理中的中斷響應與返回過程。
每次函數調用都會在棧上分配一個棧幀,包含參數、局部變量、返回地址。遞歸深度過大會導致棧溢出——這是遞歸的主要風險。
// 遞歸版
int fact_rec(int n) { return n <= 1 ? 1 : n * fact_rec(n - 1); }
// 迭代版
int fact_iter(int n) { int r = 1; while (n > 1) r *= n--; return r; }
迭代版更簡潔高效,遞歸反而增加棧開銷。
int fib_rec(int n) {
if (n <= 2) return 1;
return fib_rec(n - 1) + fib_rec(n - 2); // 指數級冗余計算
}
計算fib_rec(30)時,fib_rec(3)被重複調用317,811次!時間複雜度O(2ⁿ),極其低效。
int fib_iter(int n) {
int result = 1, prev = 1, prev2;
while (n > 2) {
n--;
prev2 = prev;
prev = result;
result = prev + prev2;
}
return result;
}
時間O(n),空間O(1),完美替代遞歸。
若遞歸調用是函數的最後一條語句(如上述二進制輸出、費波那契迭代版),稱為尾遞歸。編譯器可將其優化為跳轉指令,消除棧增長,實現O(1)空間複雜度。C語言標準不保證尾調用優化,但GCC/Clang在-O2下通常會優化。
經驗表明,只有需要逆序處理、或天然適合分治的問題才真正需要遞歸:
其他情況(階乘、費波那契、線性遞推)迭代均更優。
作者最初寫遞歸版convert2any時,苦惱於「每次調用變量銷毀,如何累加結果」,一度想用static變量,後悟出:直接用返回值傳遞累加結果!
// 遞歸版
int convert2any(int src, int dest_d, int pow_base) {
int quotient = src / dest_d;
if (quotient != 0)
return (src % dest_d) * pow(10, pow_base) + convert2any(quotient, dest_d, pow_base + 1);
else
return (src % dest_d) * pow(10, pow_base);
}
// 迭代版(尾遞歸優化)
do {
result += (src % dest_d) * pow(10, pow_base++);
} while (src /= dest_d);
迭代版更短小、高效,且無棧溢出風險。
遞歸是把雙刃劍:用得好能寫出極其優雅的代碼(如樹遍歷),用不好會導致性能災難。掌握其底層機制,才能做出正確選擇。