Java 遞迴 (Recursion)
遞迴是指方法呼叫自己的技術。
基本概念
遞迴方法需要:
- 終止條件:何時停止遞迴
- 遞迴呼叫:呼叫自己,但問題規模變小
public static void countdown(int n) {
// 終止條件
if (n <= 0) {
System.out.println("發射!");
return;
}
// 遞迴主體
System.out.println(n);
countdown(n - 1); // 遞迴呼叫
}
// countdown(3) 輸出:3, 2, 1, 發射!
階乘
public static int factorial(int n) {
// 終止條件
if (n <= 1) {
return 1;
}
// 遞迴呼叫
return n * factorial(n - 1);
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120
執行過程:
factorial(5)
= 5 * factorial(4)
= 5 * 4 * factorial(3)
= 5 * 4 * 3 * factorial(2)
= 5 * 4 * 3 * 2 * factorial(1)
= 5 * 4 * 3 * 2 * 1
= 120
費波那契數列
public static int fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
簡單遞迴的費波那契效率很差,會有大量重複計算。
優化版本(記憶化)
public static int fibonacciMemo(int n, int[] memo) {
if (n <= 1) return n;
if (memo[n] != 0) {
return memo[n]; // 已計算過
}
memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
return memo[n];
}
總和
public static int sum(int n) {
if (n <= 0) return 0;
return n + sum(n - 1);
}
// sum(5) = 5 + 4 + 3 + 2 + 1 = 15
次方
public static int power(int base, int exp) {
if (exp == 0) return 1;
return base * power(base, exp - 1);
}
// power(2, 3) = 8
最大公因數
public static int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
// gcd(48, 18) = 6
陣列操作
陣列總和
public static int arraySum(int[] arr, int index) {
if (index < 0) return 0;
return arr[index] + arraySum(arr, index - 1);
}
int[] numbers = {1, 2, 3, 4, 5};
int total = arraySum(numbers, numbers.length - 1); // 15
二分搜尋
public static int binarySearch(int[] arr, int target, int left, int right) {
if (left > right) return -1; // 找不到
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] > target) {
return binarySearch(arr, target, left, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, right);
}
}
字串操作
反轉字串
public static String reverse(String s) {
if (s.isEmpty()) return s;
return reverse(s.substring(1)) + s.charAt(0);
}
// reverse("hello") = "olleh"
檢查回文
public static boolean isPalindrome(String s) {
if (s.length() <= 1) return true;
if (s.charAt(0) != s.charAt(s.length() - 1)) {
return false;
}
return isPalindrome(s.substring(1, s.length() - 1));
}
河內塔
public static void hanoi(int n, char from, char to, char aux) {
if (n == 1) {
System.out.println("移動盤子 1 從 " + from + " 到 " + to);
return;
}
hanoi(n - 1, from, aux, to);
System.out.println("移動盤子 " + n + " 從 " + from + " 到 " + to);
hanoi(n - 1, aux, to, from);
}
// hanoi(3, 'A', 'C', 'B');
遞迴 vs 迴圈
| 特性 | 遞迴 | 迴圈 |
|---|---|---|
| 程式碼 | 通常較簡潔 | 可能較冗長 |
| 效能 | 有額外開銷 | 通常較快 |
| 記憶體 | 使用 stack | 較省記憶體 |
| 適用情境 | 樹、圖、分治 | 簡單迭代 |
階乘的迴圈版本
public static int factorialLoop(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
尾遞迴
某些語言可以優化尾遞迴,但 Java 不會:
// 尾遞迴形式
public static int factorialTail(int n, int acc) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}
// 呼叫
factorialTail(5, 1);
Stack Overflow
遞迴太深會造成 StackOverflowError:
public static void infinite(int n) {
System.out.println(n);
infinite(n + 1); // 沒有終止條件
}
// StackOverflowError!
避免方式:
- 確保有終止條件
- 確保問題規模會縮小
- 考慮使用迴圈替代