遞歸是程式設計中極具威力卻也容易讓初學者困惑的概念。本文將透過經典的「九九乘法表」範例,由淺入深說明 Java 中遞歸的運作機制、堆疊幀變化,並與迴圈寫法比較記憶體效能,幫助你徹底掌握遞歸精髓。
最直觀的實作方式是使用雙層 for 迴圈:
package test.ms;
public class Test99 {
public static void main(String[] args) {
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
System.out.print(j + " * " + i + " = " + (i * j) + " ");
}
System.out.println();
}
}
}
此寫法邏輯清晰、效能極佳,是大多數開發者的首選。
若要用遞歸實現相同功能,關鍵在於「將問題分解為子問題」:
package test.ms;
public class MultiTable {
public static void main(String[] args) {
m(9); // 從第 9 行開始遞歸
}
/**
* 遞歸列印九九乘法表
* @param i 目前要列印的行號 (1~9)
*/
public static void m(int i) {
if (i == 1) { // 基礎案例:第 1 行直接列印
System.out.println("1*1=1 ");
} else {
m(i - 1); // 先處理前 i-1 行(遞歸呼叫)
// 當前行列印邏輯
for (int j = 1; j <= i; j++) {
System.out.print(j + "*" + i + "=" + (j * i) + " ");
}
System.out.println();
}
}
}
理解遞歸的關鍵在於掌握 方法堆疊(Call Stack) 的動態變化:
main 呼叫 m(9) → m(9) 呼叫 m(8) → … → m(2) 呼叫 m(1)。每一次呼叫都會產生一個新的堆疊幀,壓入堆疊頂端。此時 i 從 9 遞減至 1,j 尚未開始執行。m(1) 符合 i == 1,直接列印 1*1=1,方法結束返回。m(1) 彈出,m(2) 繼續執行其 for 迴圈(j=1..2)列印第 2 行 → m(2) 彈出 → m(3) 列印第 3 行 … 依此類推,直到 m(9) 列印完第 9 行並彈出,程式結束。關鍵觀察:i 在每一層堆疊幀中都是獨立的區域變數,遞減於推疊階段;j 則是在各自幀彈出時才從 1 遞增至 i。正是這種「先深入到底,再層層回溯執行後續邏輯」的特性,構成了遞歸獨特的執行順序。
| 維度 | 遞歸寫法 | 雙層 for 迴圈 |
|---|---|---|
| 堆疊幀數量 | 產生 N 個堆疊幀 (N=9),佔用較多棧記憶體 | 僅 1 個 main 堆疊幀,記憶體極省 |
| 執行效能 | 方法呼叫/返回有額外開銷,稍慢 | 指令流程線性,效能最優 |
| 程式碼可讀性 | 符合數學歸納思維,邏輯優雅 | 直觀易懂,符合命令式習慣 |
| 適用場景 | 樹形結構遍歷、分治演算法、回溯問題 | 簡單線性迭代、固定次數重複 |
結論:遞歸雖優雅,但會消耗額外棧空間,深度過大易導致 StackOverflowError。九九乘法表屬於簡單迭代問題,生產環境強烈建議使用迴圈;遞歸版本僅供教學示範與理解堆疊機制。
-Xss 擴大棧大小。希望本文能幫你建立清晰的遞歸心智模型,歡迎在留言區交流心得!