递归算法是一种通过函数调用自身来解决问题的方法。简单来说,就是"自己调用自己"。递归将复杂问题分解为同类的更简单子问题,直到达到易于直接解决的基本情况。
递归算法由两个关键部分组成:
public ReturnType recursiveMethod(Parameters params) {
// 边界条件(终止条件)
if (结束条件) {
return 结束值;
}
// 递归步骤
return recursiveMethod(减小规模的参数);
}
时间复杂度:递归算法的时间复杂度主要取决于以下因素:
递归算法的时间复杂度通常可以用递推关系表示:
空间复杂度:递归算法的空间复杂度考虑两部分:
递归调用的栈空间通常是递归算法空间复杂度的主要部分,对于最大递归深度为D的算法,栈空间复杂度为O(D)。
计算 1+2+3+...+n 的和是理解递归的最佳入门示例。
public class SumExample {
public static void main(String[] args) {
// 计算1到100的和
System.out.println("1到100的和: " + getSum(100));
}
// 计算从1到n的和的递归方法
public static int getSum(int n) {
// 边界条件:当n为1时,直接返回1
if (n == 1) {
return 1;
}
// 递归步骤:n加上(n-1)的和
return n + getSum(n - 1);
}
}
以计算getSum(5)
为例,让我们看看递归的具体执行过程:
正向递归调用过程(自顶向下):
getSum(5)
,因为5 != 1
,执行return 5 + getSum(4)
,暂停等待getSum(4)
的结果getSum(4)
,因为4 != 1
,执行return 4 + getSum(3)
,暂停等待getSum(3)
的结果getSum(3)
,因为3 != 1
,执行return 3 + getSum(2)
,暂停等待getSum(2)
的结果getSum(2)
,因为2 != 1
,执行return 2 + getSum(1)
,暂停等待getSum(1)
的结果getSum(1)
,满足边界条件n == 1
,直接return 1
反向结果返回过程(自底向上):
getSum(1)
返回1getSum(2)
继续执行计算2 + 1 = 3
,返回3getSum(3)
继续执行计算3 + 3 = 6
,返回6getSum(4)
继续执行计算4 + 6 = 10
,返回10getSum(5)
继续执行计算5 + 10 = 15
,返回15,计算完成可以看到,递归过程包含两个阶段:
阶乘是另一个简单的递归实例。n的阶乘定义为:n! = n * (n-1) * (n-2) * ... * 2 * 1
。
public class FactorialExample {
public static void main(String[] args) {
int n = 5;
System.out.println(n + "的阶乘是: " + factorial(n));
}
// 计算阶乘的递归方法
public static int factorial(int n) {
// 边界条件
if (n == 0 || n == 1) {
return 1;
}
// 递归步骤
return n * factorial(n - 1);
}
}
计算factorial(4)
的步骤:
factorial(4)
→ 4 * factorial(3)
factorial(3)
→ 3 * factorial(2)
factorial(2)
→ 2 * factorial(1)
factorial(1)
→ 返回1(边界条件)factorial(2)
→ 2 * 1 = 2
factorial(3)
→ 3 * 2 = 6
factorial(4)
→ 4 * 6 = 24
时间复杂度分析:
空间复杂度分析:
斐波那契数列是一个经典递归案例,定义为:F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2) (n≥2)。
public class FibonacciExample {
public static void main(String[] args) {
int n = 7;
System.out.println("斐波那契数列第" + n + "个数是: " + fibonacci(n));
// 打印斐波那契数列的前10个数
System.out.print("斐波那契数列前10个数: ");
for (int i = 0; i < 10; i++) {
System.out.print(fibonacci(i) + " ");
}
}
// 计算斐波那契数列的递归方法
public static int fibonacci(int n) {
// 边界条件
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
// 递归步骤
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
斐波那契数列的简单递归实现存在大量重复计算,效率较低。例如计算fibonacci(5)
时,fibonacci(3)
会被重复计算多次。这个问题在后面的优化部分会详细讨论。
时间复杂度分析:
空间复杂度分析:
递归思维的核心是把大问题分解成小问题:
二分查找是在有序数组中查找特定元素的高效算法,可以使用递归实现。
public class BinarySearchExample {
public static void main(String[] args) {
int[] arr = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
int target = 11;
int result = binarySearch(arr, target, 0, arr.length - 1);
if (result != -1) {
System.out.println("元素 " + target + " 在索引 " + result + " 处");
} else {
System.out.println("元素 " + target + " 不在数组中");
}
}
// 递归实现的二分查找
public static int binarySearch(int[] arr, int target, int left, int right) {
// 边界条件:如果查找范围无效,返回-1
if (left > right) {
return -1;
}
// 计算中间索引
int mid = left + (right - left) / 2;
// 找到目标,返回索引
if (arr[mid] == target) {
return mid;
}
// 根据中间值与目标值的比较,递归查找左半部分或右半部分
if (arr[mid] > target) {
return binarySearch(arr, target, left, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, right);
}
}
}
以查找值11为例:
binarySearch(arr, 11, 0, 9)
,计算mid=4,arr[4]=9 < 11,递归调用右半部分binarySearch(arr, 11, 5, 9)
,计算mid=7,arr[7]=15 > 11,递归调用左半部分binarySearch(arr, 11, 5, 6)
,计算mid=5,arr[5]=11 == 11,返回5(找到)时间复杂度分析:
空间复杂度分析:
全排列问题是递归的经典应用。给定一组不同的数字,求其所有可能的排列。
public class PermutationExample {
public static void main(String[] args) {
String str = "ABC";
System.out.println("字符串\"" + str + "\"的全排列:");
permute(str.toCharArray(), 0, str.length() - 1);
}
// 生成全排列的递归方法
public static void permute(char[] arr, int start, int end) {
// 边界条件:当只剩一个字符时,打印当前排列
if (start == end) {
System.out.println(String.valueOf(arr));
return;
}
// 递归步骤:固定一个字符,对剩余字符进行全排列
for (int i = start; i <= end; i++) {
// 交换字符
swap(arr, start, i);
// 递归生成其余字符的排列
permute(arr, start + 1, end);
// 恢复原来的顺序(回溯)
swap(arr, start, i);
}
}
// 交换字符的辅助方法
private static void swap(char[] arr, int i, int j) {
char temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
以字符串"ABC"为例:
时间复杂度分析:
空间复杂度分析:
汉诺塔是递归的经典问题,它很好地展示了递归如何简化看似复杂的问题。
public class TowerOfHanoiExample {
public static void main(String[] args) {
int n = 3; // 盘子数量
System.out.println("移动" + n + "个盘子的步骤:");
moveDisks(n, 'A', 'B', 'C');
}
// 移动汉诺塔盘子的递归方法
public static void moveDisks(int n, char source, char auxiliary, char target) {
// 边界条件:只有一个盘子时,直接从起始柱移到目标柱
if (n == 1) {
System.out.println("将盘子1从" + source + "移动到" + target);
return;
}
// 递归步骤1:将n-1个盘子从起始柱移到辅助柱
moveDisks(n - 1, source, target, auxiliary);
// 移动最大的盘子从起始柱到目标柱
System.out.println("将盘子" + n + "从" + source + "移动到" + target);
// 递归步骤2:将n-1个盘子从辅助柱移到目标柱
moveDisks(n - 1, auxiliary, source, target);
}
}
汉诺塔问题的美妙之处在于利用递归,我们可以将复杂的多盘子问题,分解为更简单的子问题:
这里的关键是"相信"递归函数能够正确移动n-1个盘子,这样就能将复杂问题简化。
时间复杂度分析:
空间复杂度分析:
斐波那契数列的简单递归实现效率很低,因为有大量重复计算。使用记忆化技术可以显著提高效率。
public class MemoizedFibonacci {
public static void main(String[] args) {
int n = 40;
// 比较普通递归和记忆化递归的时间
long startTime = System.currentTimeMillis();
System.out.println("斐波那契(" + n + ") = " + fibonacciMemoized(n));
long endTime = System.currentTimeMillis();
System.out.println("记忆化递归耗时: " + (endTime - startTime) + "毫秒");
}
// 使用记忆化的斐波那契数列递归方法
public static long fibonacciMemoized(int n) {
// 创建备忘录数组,保存计算过的结果
long[] memo = new long[n + 1];
return fibHelper(n, memo);
}
private static long fibHelper(int n, long[] memo) {
// 边界条件
if (n <= 1) {
return n;
}
// 如果已经计算过,直接返回结果
if (memo[n] != 0) {
return memo[n];
}
// 递归计算并保存结果
memo[n] = fibHelper(n - 1, memo) + fibHelper(n - 2, memo);
return memo[n];
}
}
记忆化递归的关键思想:
复杂度优化分析:
尾递归是一种特殊的递归形式,它在函数返回时不做额外计算,直接返回递归调用的结果。这种形式更容易被编译器优化。
以阶乘计算为例:
public class TailRecursionExample {
public static void main(String[] args) {
int n = 5;
System.out.println(n + "的阶乘: " + factorial(n));
System.out.println(n + "的阶乘(尾递归): " + factorialTail(n, 1));
}
// 普通递归实现阶乘
public static int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1); // 在递归调用后还需要乘以n
}
// 尾递归实现阶乘
public static int factorialTail(int n, int result) {
if (n <= 1) {
return result;
}
return factorialTail(n - 1, n * result); // 直接返回递归调用的结果
}
}
尾递归的特点:
注意:虽然Java不会自动优化尾递归,但理解尾递归仍有助于编写更高效的代码,尤其是在支持尾递归优化的语言中。
在一些递归算法中,避免重复计算是提高效率的关键。以下是几种常见技巧:
如果忘记设置正确的终止条件,或递归时参数未正确减小,可能导致无限递归,最终导致栈溢出。
解决方法:
递归层数过多会导致栈溢出(StackOverflowError)。
解决方法:
在某些算法中,同一参数的递归调用可能被多次执行,导致不必要的计算。
解决方法:
有时,递归算法可能面临效率或栈空间限制,此时可以考虑将递归转换为迭代。
public static long fibonacciIterative(int n) {
if (n <= 1) {
return n;
}
long fib = 0;
long prev1 = 1;
long prev2 = 0;
for (int i = 2; i <= n; i++) {
fib = prev1 + prev2;
prev2 = prev1;
prev1 = fib;
}
return fib;
}
复杂度分析:
public static int factorialIterative(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
复杂度分析:
递归转迭代的一般思路:
转换后的优势:
适合使用递归的情况:
不适合使用递归的情况:
掌握递归思维不仅是掌握一种编程技巧,更是一种解决问题的思路:
递归是算法设计中的强大工具,掌握它将帮助你解决许多看似复杂的问题。通过从简单例子开始,逐步理解递归的核心概念,再结合优化技巧,你将能够写出既高效又优雅的递归算法。