一、递归思想介绍
1. 递归的本质
递归是一种自我调用的编程技巧,其核心是将复杂问题拆解为更小的相同问题,通过解决子问题最终得到原问题的解。递归的执行过程可以类比为“剥洋葱”——层层深入,再逐层返回。
2. 递归的核心逻辑
(1) 问题分解
-
目标:将大问题拆分成多个结构相同的子问题。
-
示例:
- 爬楼梯问题:爬到第
n阶楼梯的方式数 = 爬到第n-1阶的方式数(最后一步爬1阶) + 爬到第n-2阶的方式数(最后一步爬2阶)。 - 二叉树前序遍历:处理当前节点后,分别递归处理左子树和右子树。
- 爬楼梯问题:爬到第
(2) 状态回溯
-
递归调用栈:每次递归调用都会将当前状态(如参数、局部变量)压入调用栈,当子问题解决后,从栈顶回溯到上一层状态,继续执行后续逻辑。
-
示例:
- 在爬楼梯问题中,计算
climb(5)时,会依次调用climb(4)和climb(3),直到到达终止条件n <= 2,然后逐步返回结果并累加。
- 在爬楼梯问题中,计算
二、案例分析:前序遍历与爬楼梯问题
案例一:二叉树的前序遍历
问题描述
给定一棵二叉树,要求按前序遍历的顺序输出所有节点的值。
前序遍历定义:根节点 → 左子树 → 右子树。
1. 解题思路与原理
递归解法的核心思想
- 问题分解:将大问题拆解为处理当前节点、左子树、右子树三个子问题。
- 终止条件:当当前节点为空时,直接返回(无需处理)。
- 递归关系:
- 先处理当前节点(加入结果数组)。
- 再递归处理左子树。
- 最后递归处理右子树。
以如下二叉树为例:
前序遍历顺序:A → B → D → E → C → F
执行流程:
-
访问根节点 A → 加入结果
-
递归处理左子树 B: a. 访问 B → 加入结果 b. 递归处理 B 的左子树 D → 加入结果 c. 递归处理 B 的右子树 E → 加入结果
-
递归处理右子树 C: a. 访问 C → 加入结果 b. 递归处理 C 的右子树 F → 加入结果
2. 代码实现
function preorderTraversal(root) {
const result = []; // 存储遍历结果
function dfs(node) {
if (!node) return; // 终止条件:空节点无需处理
result.push(node.val); // 根
dfs(node.left); // 左
dfs(node.right); // 右
}
dfs(root); // 从根节点开始递归
return result;
}
3. 代码分析
(1) 函数定义
preorderTraversal(root):主函数,接收二叉树的根节点root,返回前序遍历结果。dfs(node):递归函数,处理以node为根的子树。
(2) 终止条件
if (!node) return;:当当前节点为空时,直接返回,防止无限递归。
(3) 递归关系
- 根节点处理:
result.push(node.val)将当前节点的值加入结果数组。 - 左子树递归:
dfs(node.left)处理左子树。 - 右子树递归:
dfs(node.right)处理右子树。
(4) 执行流程
- 初始调用
dfs(root)。 - 处理根节点 A,加入结果数组。
- 递归调用
dfs(B),处理左子树。 - 在
dfs(B)中,处理 B,加入结果,递归调用dfs(D)。 - 在
dfs(D)中,处理 D,加入结果,左右子树为空,返回。 - 返回到
dfs(B),继续递归调用dfs(E),处理 E,加入结果。 - 返回到
dfs(A),递归调用dfs(C),处理 C,加入结果,递归调用dfs(F)。 - 最终返回结果数组
[A, B, D, E, C, F]。
(5) 时间与空间复杂度
- 时间复杂度:O(n),每个节点访问一次。
- 空间复杂度:O(h),
h为树的高度(递归调用栈的空间)。
案例二:爬楼梯问题
问题描述
假设你正在爬楼梯,每次可以爬 1 阶或 2 阶。问:爬到第 n 阶楼梯有多少种不同的方法?
1. 解题思路与原理
递归解法的核心思想
-
问题分解:到达第
n阶的方法数 = 到达第n-1阶的方法数(最后一步爬 1 阶) + 到达第n-2阶的方法数(最后一步爬 2 阶)。 -
终止条件:当
n <= 2时,直接返回n(因为n=1有 1 种方法,n=2有 2 种方法)。 -
递归关系:
climbStairs(n) = climbStairs(n-1) + climbStairs(n-2)。
以 n=4 为例:
最终结果:3 + 2 = 5(方法数)。
2. 代码实现
function climbStairs(n) {
if (n <= 2) return n; // 终止条件
return climbStairs(n - 1) + climbStairs(n - 2); // 递归关系
}
3. 代码详细分析
(1) 函数定义
climbStairs(n):主函数,接收楼梯阶数n,返回到达第n阶的方法数。
(2) 终止条件
if (n <= 2) return n;:当n=1或n=2时,直接返回n(基准条件)。
(3) 递归关系
climbStairs(n - 1):从第n-1阶爬 1 阶到达第n阶。climbStairs(n - 2):从第n-2阶爬 2 阶到达第n阶。
(4) 执行流程
- 调用
climbStairs(4)。 - 分解为
climbStairs(3) + climbStairs(2)。 climbStairs(3)分解为climbStairs(2) + climbStairs(1)。climbStairs(2)返回 2,climbStairs(1)返回 1。- 最终结果:
2 + 1 + 2 = 5。
(5) 时间与空间复杂度
- 时间复杂度:O(2^n),存在大量重复计算(如
climbStairs(3)被多次调用)。 - 空间复杂度:O(n),递归调用栈的最大深度为
n。
三、递归与遍历的典型应用场景
1. 树结构问题
- 二叉树遍历:前序、中序、后序(如序列化树、复制树)。
- 树的构造:通过前序+中序或后序+中序还原树。
- 路径问题:寻找路径和、最长路径。
2. 序列问题
- 斐波那契数列:经典递归问题(如爬楼梯的简化版)。
- 组合问题:生成所有组合(如全排列、子集)。
- 分治算法:将问题拆解为独立子问题(如快速排序、归并排序)。
四、总结:递归思维的通用性与局限性
1. 递归的优势
- 代码简洁:直接映射问题定义(如树遍历、斐波那契)。
- 逻辑清晰:天然符合人类分步解决问题的思维方式。
2. 递归的局限性
- 效率问题:未优化的递归可能产生大量重复计算(如
climb(5)调用climb(3)两次)。 - 栈溢出风险:深度过大的递归可能导致调用栈溢出(如链状树)。
3. 递归 vs 动态规划的选择
- 选择递归:问题规模较小、子问题无重叠(如二叉树遍历)。
- 选择动态规划:存在大量重叠子问题(如爬楼梯、背包问题)。