04_递归方法原理与应用
一、递归的基本概念
1.1 什么是递归
递归(Recursion)是指方法在执行过程中直接或间接调用自身的编程技巧。它通过将复杂问题分解为与原问题相似的更小子问题,最终通过解决子问题得到原问题的解。
例如,计算n!(阶乘)时,可利用递归思想:n! = n × (n-1)!,其中(n-1)!是n!的子问题,与原问题结构一致。
1.2 递归的核心要素
递归能正常工作,必须满足两个核心要素:
- 终止条件(Base Case):
当问题分解到最小规模时,直接返回结果,不再递归调用(避免无限递归)。
例如:n!的终止条件是n=0或n=1时,0! = 1,1! = 1。 - 递归体(Recursive Case):
将原问题分解为规模更小的子问题,并通过调用自身解决子问题,最终组合子问题的结果得到原问题的解。
例如:n!的递归体是n × factorial(n-1)。
![在这里插入图片描述]()
二、递归的工作原理
2.1 执行流程
递归的执行过程分为两个阶段:
- 递推阶段:
方法不断调用自身,将原问题分解为更小的子问题,直到达到终止条件。每一次调用都会在内存的调用栈中创建新的方法帧(存储参数、局部变量等)。 - 回归阶段:
当达到终止条件后,方法开始逐层返回结果,从最小子问题的解逐步计算出原问题的解,调用栈中的方法帧依次出栈。
2.2 调用栈机制
递归依赖 Java 的方法调用栈(Stack)实现,每次递归调用都会产生以下操作:
- 在栈顶创建新的方法帧(包含当前方法的参数、局部变量、返回地址等)。
- 当达到终止条件时,当前方法帧计算结果并返回,方法帧出栈。
- 上层方法接收返回值,继续执行剩余逻辑,直至栈中所有方法帧出栈,得到最终结果。
![在这里插入图片描述]()
2.3 示例解析:阶乘计算
以n!(n的阶乘)为例,理解递归执行流程:
// 计算n的阶乘(n! = n × (n-1) × ... × 1)
public static int factorial(int n) {
// 终止条件:n=0或n=1时,直接返回1
if (n == 0 || n == 1) {
return 1;
}
// 递归体:n! = n × (n-1)!
return n * factorial(n - 1);
}
// 调用
public static void main(String[] args) {
System.out.println(factorial(5)); // 输出120(5! = 5×4×3×2×1)
}
执行流程说明:
- factorial(5)调用factorial(4);
- factorial(4)调用factorial(3);
- 以此类推,直至factorial(1)触发终止条件,返回 1;
- 逐层回归:factorial(2)=2×1=2 → factorial(3)=3×2=6 → ... → factorial(5)=5×24=120。
三、递归的应用场景
递归适合解决具有递归结构的问题(即问题可分解为相似的子问题),常见场景包括:
3.1 数学问题
示例 1:斐波那契数列(Fibonacci)
斐波那契数列定义:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2)。
public static int fibonacci(int n) {
// 终止条件
if (n == 0) return 0;
if (n == 1) return 1;
// 递归体:F(n) = F(n-1) + F(n-2)
return fibonacci(n - 1) + fibonacci(n - 2);
}

示例 2:最大公约数(GCD,欧几里得算法)
利用定理:gcd(a,b) = gcd(b, a%b),终止条件为b=0时,gcd(a,0)=a。
public static int gcd(int a, int b) {
// 终止条件:b=0时,返回a
if (b == 0) return a;
// 递归体:gcd(a,b) = gcd(b, a%b)
return gcd(b, a % b);
}
3.2 数据结构相关问题
示例 1:二叉树遍历(前序 / 中序 / 后序)
二叉树的节点结构天然具有递归性(每个节点的左 / 右子树仍是二叉树),递归遍历代码简洁清晰。
// 二叉树节点定义
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
}
// 前序遍历(根→左→右)
public static void preorder(TreeNode node) {
// 终止条件:节点为null时,停止递归
if (node == null) return;
// 访问根节点
System.out.print(node.val + " ");
// 递归遍历左子树
preorder(node.left);
// 递归遍历右子树
preorder(node.right);
}

示例 2:链表反转
通过递归将链表分解为 “头节点” 和 “剩余子链表”,反转子链表后再拼接头节点。
// 链表节点定义
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
// 递归反转链表
public static ListNode reverseList(ListNode head) {
// 终止条件:空链表或单节点链表,直接返回自身
if (head == null || head.next == null) {
return head;
}
// 递归反转剩余子链表
ListNode newHead = reverseList(head.next);
// 拼接头节点与反转后的子链表
head.next.next = head;
head.next = null;
return newHead;
}
3.3 经典算法问题
示例:汉诺塔(Hanoi Tower)
汉诺塔问题要求将n个盘子从 A 柱移到 C 柱,中间可借助 B 柱,且大盘不能放在小盘上。递归思路:
- 将n-1个盘子从 A→B;
- 将第n个盘子从 A→C;
- 将n-1个盘子从 B→C。
/**
* 汉诺塔递归实现
* @param n 盘子数量
* @param from 起始柱
* @param mid 中间柱
* @param to 目标柱
*/
public static void hanoi(int n, char from, char mid, char to) {
// 终止条件:只有1个盘子时,直接从from移到to
if (n == 1) {
System.out.println("移盘子 " + n + " 从 " + from + " 到 " + to);
return;
}
// 步骤1:将n-1个盘子从from移到mid(借助to)
hanoi(n - 1, from, to, mid);
// 步骤2:将第n个盘子从from移到to
System.out.println("移盘子 " + n + " 从 " + from + " 到 " + to);
// 步骤3:将n-1个盘子从mid移到to(借助from)
hanoi(n - 1, mid, from, to);
}
// 调用:移动3个盘子从A到C
hanoi(3, 'A', 'B', 'C');
四、递归的优缺点
4.1 优点
- 代码简洁直观:递归能直接反映问题的数学定义或逻辑结构(如斐波那契、二叉树遍历),比迭代更易理解。
- 适合分治问题:对于可分解为相似子问题的场景(如汉诺塔、归并排序),递归是天然的解决方案。
4.2 缺点
- 栈溢出风险:每次递归调用都会创建新的栈帧,若递归深度过大(如n>10000),会触发StackOverflowError。
- 效率问题:
- 存在重复计算(如斐波那契数列的fib(5)会重复计算fib(3)、fib(2)等)。
- 栈帧创建和销毁有额外时间开销。
- 调试困难:递归调用链长,调试时难以跟踪每个栈帧的状态。
五、递归的注意事项与优化
5.1 避免栈溢出
- 控制递归深度:确保问题规模不会导致递归深度超过 JVM 栈的最大容量(默认栈深度通常在几千到几万级别)。
- 改用迭代:对深度较大的问题(如大规模数据遍历),优先用迭代(for/while)替代递归。
5.2 减少重复计算
- 记忆化搜索(Memoization):用数组或哈希表缓存已计算的子问题结果,避免重复计算。
例如优化斐波那契数列:
// 缓存子问题结果(记忆化)
private static int[] memo;
public static int fibonacciOptimized(int n) {
// 初始化缓存
if (memo == null) {
memo = new int[n + 1];
// 初始化缓存为-1(表示未计算)
Arrays.fill(memo, -1);
memo[0] = 0;
memo[1] = 1;
}
// 若已计算,直接返回缓存结果
if (memo[n] != -1) {
return memo[n];
}
// 递归计算并缓存结果
memo[n] = fibonacciOptimized(n - 1) + fibonacciOptimized(n - 2);
return memo[n];
}
5.3 尾递归优化(局限性)
尾递归是指递归调用是方法的最后一步操作(无后续计算)。理论上,尾递归可被编译器优化为循环(避免栈溢出),但Java 编译器不支持尾递归优化,因此实际开发中需谨慎依赖。
// 尾递归版本的阶乘(Java仍可能栈溢出)
public static int factorialTail(int n, int result) {
if (n == 0) return result;
// 递归调用是最后一步,无后续计算(尾递归形式)
return factorialTail(n - 1, n * result);
}
// 调用:初始result为1
factorialTail(5, 1); // 等价于5! = 5×4×3×2×1×1
六、总结
递归是 Java 中强大的编程技巧,其核心是 “分解问题→终止条件→组合子问题解”:
- 适用场景:数学定义问题(阶乘、斐波那契)、递归数据结构(二叉树、链表)、分治算法(汉诺塔、归并排序)。
- 关键要素:必须包含终止条件(避免无限递归)和递归体(分解问题)。
- 注意事项:警惕栈溢出和重复计算,必要时用迭代或记忆化搜索优化。
合理使用递归能简化代码,但需在可读性和性能之间平衡 —— 对于深度大或性能敏感的场景,迭代往往是更稳妥的选择。



浙公网安备 33010602011771号