在递归算法的世界里,我们通常先接触 “单路递归”—— 比如斐波那契数列(虽然优化后常用迭代),每次递归仅调用自身一次。但当问题需要拆解为多个子问题依次求解时,“多路递归” 就成了更强大的工具。它能将复杂问题拆解为结构相似的多个子任务,通过组合子任务的结果得到最终答案。
本文将从多路递归的核心概念入手,结合汉诺塔(经典移动问题)和杨辉三角(组合数计算)两个案例,带你掌握多路递归的设计思路、代码实现与优化技巧,最终能独立解决类似的递归问题。
一、什么是多路递归?先理清核心概念
在正式讲案例前,我们先明确“多路递归”的定义,避免和其他递归类型混淆:
- 单路递归:函数在递归过程中,仅调用自身一次。例如求 n 的阶乘(f(n) = n * f(n-1)),每次递归只依赖一个子问题的结果。
- 多路递归:函数在递归过程中,调用自身两次及以上。例如本文要讲的汉诺塔,每次移动都需要调用两次递归函数处理不同柱子的盘子;杨辉三角的每个元素,也需要依赖上方两个元素的递归结果。
多路递归的核心思想是“分而治之”:将原问题拆解为多个规模更小、结构相同的子问题,分别求解后合并结果。其关键在于两点:
1、找到递归边界:当子问题规模小到可以直接求解时,停止递归(避免无限循环);
2、明确递归关系:原问题与子问题之间的数学/逻辑关联
二、案例一:汉诺塔——理解多路递归的 “任务拆分”
汉诺塔是递归算法的 “入门经典”,也是理解多路递归 “任务拆分逻辑” 的最佳案例。我们先明确问题规则,再逐步推导递归思路。
1、汉诺塔问题规则
有3根柱子(A、B、C)和n个大小不同的盘子(从小到大编号 1~n),初始时所有盘子都叠在A柱上(大的在下,小的在上)。要求:
- 每次只能移动 1 个盘子;
- 任何时刻都不能让 “大盘子在小盘子上方”;
- 最终将所有盘子从A柱移动到C柱(B 柱作为辅助)。
2. 递归思路:从 “小问题” 推导 “大问题”
直接思考n个盘子的移动方式很复杂,我们可以先从小规模问题入手,找到规律:
(1)当 n=1 时(边界条件)
只有 1 个盘子,直接从 A 移到 C 即可,步骤:A → C。
(2)当 n=2 时
需要借助 B 柱,分 3 步:
- 把盘子 1(小)从 A → B;
- 把盘子 2(大)从 A → C;
- 把盘子 1(小)从 B → C。
此时发现:移动 2 个盘子的核心是 “先把上方小盘子移到辅助柱,再把下方大盘子移到目标柱,最后把小盘子从辅助柱移到目标柱”。
(3)当 n=3 时
基于 n=2 的思路,我们可以把 “上方 2 个盘子” 看作一个整体,拆解为 3 个核心任务:
- 把 A 柱上的“盘子1 + 盘子2”移到 B 柱(此时 C 柱作为辅助,复用 n=2 的逻辑);
- 把 A 柱上剩下的“盘子 3”(最大的)移到 C 柱;
- 把 B 柱上的“盘子1 + 盘子2”移到 C 柱(此时 A 柱作为辅助,再次复用 n=2 的逻辑)。
此时,n=3 的问题被拆解为 “2 个 n=2 的子问题”+“1 个直接移动”,这就是多路递归的核心——用多个小规模子问题的解,组合成原问题的解。
图 1:汉诺塔 n=3 时的核心移动过程 —— 通过三步操作,完成从 “a柱初始状态” 到 “c柱最终叠放” 的转移,直观体现多路递归的 “子问题拆分与组合” 逻辑。
(4)推广到 n 个盘子
对于 n 个盘子,递归关系可总结为:
- 子问题 1:将 A 柱上的 n-1 个盘子,通过 C 柱辅助,移动到 B 柱;
- 直接操作:将 A 柱上剩下的第 n 个盘子(最大),移动到 C 柱;
- 子问题 2:将 B 柱上的 n-1 个盘子,通过 A 柱辅助,移动到 C 柱。
3、代码实现与验证
我们用Java实现上述逻辑,并测试 n=3 的情况
3.1 代码完整展示
import java.util.LinkedList;
public class HanoiTower {
static LinkedList<Integer> a = new LinkedList<>();
static LinkedList<Integer> b = new LinkedList<>();
static LinkedList<Integer> c = new LinkedList<>();
static void init(int n){
for (int i = n; i >= 1; i--) {
a.addLast(i);
}
}
static void move(int n,LinkedList<Integer> a,LinkedList<Integer> b,LinkedList<Integer> c){
if(n==0){
return;
}
move(n-1,a,c,b);
c.addLast(a.removeLast());
print();
move(n-1,b,a,c);
}
public static void main(String[] args) {
init(3);
print();
move(3,a,b,c);
}
private static void print() {
System.out.println("----------");
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
3.2 代码结构解析
(1)数据结构定义
static LinkedList<Integer> a = new LinkedList<>();
static LinkedList<Integer> b = new LinkedList<>();
static LinkedList<Integer> c = new LinkedList<>();
- 用LinkedList模拟三根柱子(a、b、c),其中存储的Integer表示圆盘的大小(数字越大,圆盘越大)。
- LinkedList的addLast()和removeLast()方法模拟从柱子顶端添加/移除圆盘(符合栈的“后进先出”特性)。
(2)初始化方法 init(int n)
static void init(int n){
for (int i = n; i >= 1; i--) {
a.addLast(i);
}
}
- 功能:初始化 a 柱,将n个圆盘按从小到大的顺序放入 a 柱(例如 n=3 时,a 柱为[3,2,1],3在最底部,1在顶端)。
(3)递归移动方法 move(...)
/**
* @param n 圆盘个数
* @param a 源头
* @param b 辅助
* @param c 目标
*/
static void move(int n, LinkedList<Integer> a, LinkedList<Integer> b, LinkedList<Integer> c) {
if (n == 0) {
return; //没有圆盘需要移动时直接返回
}
//步骤1:将n-1个圆盘从a柱通过c柱的辅助,移动到b柱
move(n-1,a,c,b);
// 步骤2:将第n个圆盘(最大的那个)从a柱直接移动到c柱
c.addLast(a.removeLast());
print(); // 打印移动后的状态
//步骤3:将n-1个圆盘从b柱通过a柱的辅助,移动到c柱
move(n-1,b,a,c);
}
-
递归思想:将 n 个圆盘的移动分解为 3 步(利用递归将大问题拆解为小问题):
- 先把上方 n-1 个圆盘,借助目标柱 c,从源柱 a 移到辅助柱 b ;
- 把最底部的第 n 个圆盘(最大)从源柱 a 直接移到目标柱 c ;
- 再把 n-1 个圆盘,借助源柱 a ,从辅助柱 b 移到目标柱 c 。
-
时间复杂度:递归公式为
T(n) = 2*T(n-1) + c,解得时间复杂度为O(2ⁿ)(移动 n 个圆盘需要 2ⁿ-1 步)。
(4)打印方法 print()
private static void print() {
System.out.println("----------");
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
- 功能:打印每一步移动后三根柱子的状态(展示圆盘的分布),便于观察移动过程;
(5)主方法 main(...)
public static void main(String[] args) {
init(3); // 初始化3个圆盘到a柱
print(); // 打印初始状态
move(3, a, b, c); // 开始移动3个圆盘(从a到c,借助b)
}
- 流程:初始化 3 个圆盘到 a 柱,打印初始状态后,调用
move方法开始移动,最终所有圆盘将从 a 柱移到 c 柱。
3.3 执行结果示例(n=3 时)
[3, 2, 1]
[]
[]
----------
[3, 2]
[]
[1]
----------
[3]
[2]
[1]
----------
[3]
[2, 1]
[]
----------
[]
[2, 1]
[3]
----------
[1]
[2]
[3]
----------
[1]
[]
[3, 2]
----------
[]
[]
[3, 2, 1]
- 每一步移动后会打印状态,最终 C 柱会变为
[3,2,1],完成移动。
4、复杂度分析
- 时间复杂度:每次移动 n 个盘子需要 2 次移动 n-1 个盘子的操作(子问题 1 和子问题 2),再加上 1 次直接移动。递推公式为
T(n) = 2*T(n-1) + 1,边界条件T(1)=1。解得T(n) = 2ⁿ - 1,即O(2ⁿ)
- 空间复杂度:递归调用栈的深度为 n(每次递归 n 减 1,直到 n=1),因此空间复杂度为O(n)。
三、案例二:杨辉三角 —— 多路递归的 “结果合并”
杨辉三角(也称帕斯卡三角)是组合数学中的经典模型,其每个元素的值等于上方两个相邻元素之和,这一特性天然适配多路递归的 “多子问题结果合并” 逻辑。我们先明确杨辉三角的规则,再结合你提供的 Java 代码推导实现思路。
1、杨辉三角的核心规则
杨辉三角的结构满足以下 3 个条件:
- 第 0 行只有 1 个元素,值为 1(行号从 0 开始,方便后续递归计算);
- 每一行的第 0 个元素和最后 1 个元素,值均为 1;
- 对于第 n 行第 k 个元素(0 < k < n+1),其值等于第 n-1 行第 k-1 个元素与第 n-1 行第 k 个元素之和,即 C(n,k) = C(n-1,k-1) + C(n-1,k)(C(n,k) 表示从 n 个元素中选 k 个的组合数)。
2、递归思路
我们可以从 “递归计算单个元素” 和 “格式化打印三角” 两个维度实现杨辉三角,核心思路与多路递归逻辑完全契合:
(1)维度一:递归计算单个元素的值
通过 element(int i, int j) 方法实现,核心是明确递归边界与递归关系:
- 递归边界:当
j == 0(每行第 0 个元素)或i == j(每行最后 1 个元素)时,元素值为 1;
- 递归关系:对于 0 < j < i,元素值 = 上一行左侧元素(i-1 行 j-1 列) + 上一行右侧元素(i-1 行 j 列),即
element(i,j) = element(i-1,j-1) + element(i-1,j),这正是多路递归的典型应用 —— 依赖两个子问题结果合并得到当前结果。
(2)维度二:格式化打印完整三角
通过两个辅助方法配合实现:
printSpace(int n, int i):根据总行数 n 和当前行号 i,计算并打印每行左侧的空格,实现三角居中效果(空格数量为 (n-1-i)*2,确保不同行数的三角结构对称);
print(int n):循环生成每行元素,先调用 printSpace 打印左侧空格,再通过嵌套循环调用 element 方法获取每行每个元素的值,并用printf("%-4d", element(i,j))格式化元素(左对齐,占 4 个字符位),保证元素排列整齐。
3、代码实现:基于 Java 的完整实现
public class PascalTriangle{
/**
* 多路递归:计算杨辉三角第i行第j列的元素值
* @param i 行号(从0开始)
* @param j 列号(从0开始)
* @return 第i行第j列的元素值
*/
private static int element(int i,int j){
if(j==0 || i==j){
return 1;
}
return element(i-1,j-1)+element(i-1,j);
}
/**
* 辅助方法:打印每行左侧的空格,实现三角居中效果
* @param n 杨辉三角的总行数
* @param i 当前行号(从0开始)
*/
public static void printSpace(int n,int i){
int num = (n-1-i)*2;
for (int j = 0; j < num; j++) {
System.out.print(" ");
}
}
public static void print(int n){
//循环遍历每一行(行号从0到n-1)
for (int i = 0; i < n; i++) {
//先打印当前行左侧的空格,确保居中
printSpace(n,i);
//循环遍历当前行的每一列(列号从0到i)
for (int j = 0; j <= i; j++) {
System.out.printf("%-4d",element(i,j));
}
//一行打印完成后换行
System.out.println();
}
}
//主方法:程序入口,测试打印10行杨辉三角
public static void main(String[] args) {
print(10);
}
}
4. 代码运行结果
在 Java 环境中运行上述代码,将输出 10 行居中对齐的杨辉三角,结果如下
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
1 9 36 84 126 126 84 36 9 1
5. 复杂度分析与优化:避免重复计算
上述 Java 代码的核心逻辑存在 “大量重复计算” 的问题:例如计算第 5 行第 2 个元素(值为 10)时,会重复计算第 4 行第 1 个元素(值为 4)、第 3 行第 0 个元素(值为 1)等,导致随着总行数 n 增大,计算效率明显下降。
(1)基础实现的复杂度
- 时间复杂度:计算单个元素的时间为 O(2ⁱ)(i 为当前行号),生成 n 行三角的总时间为 O(n·2ⁿ),当 n=20 时,计算耗时会显著增加;
- 空间复杂度:递归调用栈的深度为 n(计算第 n-1 行元素时,栈深度达到最大),同时存储打印结果无需额外空间,总空间复杂度为 O(n)。
(2)二维数组缓存避免重复计算
核心:通过二维数组缓存已计算的元素值,让每个元素 (i,j) 仅计算一次:
- 初始化二维数组 triangle,初始值均为 0(未计算状态);
- 计算元素前先判断 triangle[i][j] 是否大于 0,若大于 0 则直接返回(已计算过);
- 计算完成后将结果存入 triangle[i][j],供后续调用复用。
代码实现如下:
public class PascalTriangle{
/**
* @param triangle 二维数组:缓存已计算的元素值,避免重复递归
* @param i 行号(从0开始)
* @param j 列号(从0开始)
* @return 第i行第j列的元素值
*/
private static int element(int[][] triangle,int i,int j){
//缓存判断:若该位置已计算过(值>0),直接返回(核心优化点)
if(triangle[i][j]>0){
return triangle[i][j];
}
if(j==0 || i==j){
triangle[i][j]=1;
return 1;
}
triangle[i][j]=element(triangle,i-1,j-1)+element(triangle,i-1,j);
return triangle[i][j];
}
public static void printSpace(int n,int i){
int num = (n-1-i)*2;
for (int j = 0; j < num; j++) {
System.out.print(" ");
}
}
public static void print(int n){
//二维数组初始化后元素默认值为 0
int[][] triangle =new int[n][];
for (int i = 0; i < n; i++) {
triangle[i]=new int[i+1];
//打印当前行左侧空格,确保三角居中
printSpace(n,i);
for (int j = 0; j <= i; j++) {
//循环获取当前行每个元素的值(依赖缓存,无重复计算)
System.out.printf("%-4d",element(triangle,i,j));
}
System.out.println();
}
}
public static void main(String[] args) {
print(10);
}
}
(3)复杂度对比
| 对比维度 | 基础版(含重复计算) | 优化版(二维数组缓存) |
|---|---|---|
| 时间复杂度(单个元素) | 计算第 i 行第 j 个元素时,因每个元素依赖两个子元素,递归调用次数呈指数增长,时间复杂度为 O(2ⁱ) | 每个元素 (i,j) 仅计算一次,后续直接从数组缓存读取,时间复杂度降至 O(1) |
| 时间复杂度(整体) | 生成前 n 行需计算 n(n+1)/2 个元素,总时间复杂度为 O(n·2ⁿ) | 生成前 n 行仅需计算 n(n+1)/2 次,总时间复杂度为 O(n²) |
| 空间复杂度 | 仅需递归调用栈存储调用关系,栈深度最大为 n-1(即最大行号),空间复杂度为 O(n) | 需额外维护二维数组 triangle(大小为 n(n+1)/2),叠加递归调用栈空间,总空间复杂度为 O(n²) |
| 核心差异 | 无缓存导致大量重复计算,时间效率极低,仅适合小规模场景 | 用空间换时间,通过数组缓存消除重复计算,时间效率大幅提升,支持中大规模场景 |
四、总结:掌握多路递归,解锁复杂问题求解能力
通过汉诺塔与杨辉三角案例可知,多路递归本质是 “分治 + 合并”,将原问题拆解为 2 个及以上相似子问题后组合结果,区别于单路递归的 “线性依赖”,设计时需明确递归边界与递归关系。
从案例中还能得到启示,汉诺塔可借 “整体代换” 拆解多步骤依赖任务,杨辉三角可用缓存(如二维数组)将时间复杂度从 O (n·2ⁿ) 降至 O (n²) 实现 “空间换时间”,正如笛卡尔所说:“把每个复杂的问题分解成尽可能简单的部分,然后逐个解决,再将结果组合起来,就能攻克最困难的挑战。”