一、二叉树的中序遍历(递归 + 迭代双解法)
1. 题目核心定义
- 遍历定义 :中序遍历的顺序是 左子树 → 根节点 → 右子树,即先访问左子树的所有节点,再访问根节点,最后访问右子树的所有节点。
- 输入 :一棵二叉树的根节点
root。 - 输出:按中序遍历顺序排列的节点值列表。
- 边界情况 :输入为空树(
root == null)时,返回空列表。
2. 核心解法逻辑
方法一:递归解法(最直观)
- 核心思想:利用递归的栈调用天然模拟遍历顺序,先递归处理左子树,再记录当前节点值,最后递归处理右子树。
- 执行流程
- 若当前节点为空,直接返回;
- 递归遍历当前节点的左子树;
- 将当前节点的值加入结果列表;
- 递归遍历当前节点的右子树。
- 关键规则:递归终止条件必须判断节点是否为空,否则会出现空指针异常。
方法二:迭代解法(面试高频)
- 核心思想:用栈手动模拟递归调用栈,通过指针追踪当前节点,优先深入左子树,再回溯处理根节点,最后转向右子树。
- 执行流程
- 初始化栈和结果列表,指针
cur指向根节点; - 当
cur不为空或栈不为空时,循环执行:- 若
cur不为空,将cur入栈,cur移动到其左子节点(持续深入左子树); - 若
cur为空,弹出栈顶节点,将节点值加入结果列表,cur移动到该节点的右子节点;
- 若
- 循环结束,返回结果列表。
- 初始化栈和结果列表,指针
- 关键规则:必须先处理完所有左子节点,再处理根节点,最后处理右子节点,严格遵循中序顺序。
3. 标准模板代码(Java 版)
递归实现
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root, res);
return res;
}
private void inorder(TreeNode node, List<Integer> res) {
if (node == null) {
return;
}
inorder(node.left, res); // 遍历左子树
res.add(node.val); // 访问根节点
inorder(node.right, res); // 遍历右子树
}
}
迭代实现
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
// 持续深入左子树
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
// 左子树处理完毕,弹出根节点
cur = stack.pop();
res.add(cur.val);
// 转向右子树
cur = cur.right;
}
return res;
}
}
4. 代码关键细节
- 递归解法 :
- 必须传递结果列表作为参数,避免频繁创建新集合导致性能损耗;
- 终止条件
node == null是递归退出的核心,不可省略。
- 迭代解法 :
- 使用
Deque作为栈(Java 中推荐用LinkedList实现),避免使用Stack类; - 内层循环负责将所有左子节点入栈,确保左子树优先处理;
- 弹出节点后必须将
cur指向其右子节点,以继续处理右子树。
- 使用
- 空树处理:两种解法都天然支持空树输入,直接返回空列表,无需额外判断。
5. 复杂度分析(面试必说)
- 时间复杂度 :
O(n),其中n是二叉树的节点数。每个节点都被访问且仅访问一次。 - 空间复杂度 :
- 递归解法:
O(h),h是二叉树的高度,递归调用栈的深度取决于树的高度,最坏情况(链状树)为O(n)。 - 迭代解法:
O(h),栈的最大存储深度等于树的高度,最坏情况为O(n)。
- 递归解法:
6. 典型场景验证
场景 1:普通二叉树
1
\
2
/
3
- 中序遍历结果:
[1, 3, 2] - 递归执行路径:
inorder(1) → inorder(null) → 加入1 → inorder(2) → inorder(3) → inorder(null) → 加入3 → inorder(null) → 加入2 - 迭代执行路径:
1入栈 → cur=1.right=2 → 2入栈 → cur=2.left=3 → 3入栈 → cur=3.left=null → 弹出3(加入3)→ cur=3.right=null → 弹出2(加入2)→ cur=2.right=null → 弹出1(加入1)→ cur=1.right=2(已处理)
场景 2:空树
- 输入:
root = null - 输出:
[]
场景 3:单节点树
- 输入:
root = new TreeNode(5) - 输出:
[5]
7. 面试答题话术
我会用递归 + 迭代两种方法解决二叉树中序遍历问题:
- 递归解法 最直观,核心是遵循「左→根→右」的顺序,先递归左子树,再记录根节点值,最后递归右子树,时间复杂度
O(n),空间复杂度由递归栈深度决定。- 迭代解法 是面试重点,用栈模拟递归过程,先把所有左子节点入栈,再弹出节点记录值,最后转向右子树,同样保证时间复杂度
O(n),空间复杂度O(h)。两种解法都能覆盖空树、单节点、普通树等所有场景,迭代解法更能体现对栈和遍历过程的理解。
二、二叉树相同性判断
1、题目核心定义
- 问题描述 :给定两棵二叉树的根节点
p和q,判断这两棵树是否完全相同(结构相同 + 对应节点值相同)。 - 核心要求 :
- 结构相同:每个位置的节点要么都为空,要么都不为空;
- 值相同:所有非空对应节点的
val完全一致。
- 边界情况 :
- 两棵树都为空 → 返回
true; - 一棵为空一棵非空 → 返回
false。
- 两棵树都为空 → 返回
2、核心解法逻辑(最优:逐节点递归同步校验)
1. 核心思路
- 递归终止锚点 :二叉树的叶子节点左右子节点都是
null,这是树的天然边界,必须以此作为终止条件(否则递归无限执行); - 校验优先级:先校验结构(节点是否为空),再校验值(避免空指针异常),最后递归校验子树;
- 同步遍历特性 :不是传统前 / 中 / 后序遍历(单树收集值),而是 "双树同步深度优先校验 "------ 同步走两棵树的同一路径,逐节点核对,不匹配立刻终止。
2. 执行流程(分步拆解)
- 终止条件 1 :若
p == null && q == null→ 两棵树同步走到边界,结构匹配,返回true; - 终止条件 2 :若
p == null || q == null→ 结构不匹配(能走到这步说明不是都空),返回false; - 值校验 :若
p.val != q.val→ 对应节点值不同,返回false; - 递归校验子树 :同步校验左子树 + 同步校验右子树(必须都匹配才返回
true,利用&&短路特性提前终止)。
3. 为什么不选 "遍历存结果再对比"?
| 方案 | 时间复杂度 | 空间复杂度 | 逻辑缺陷 | 核心问题 |
|---|---|---|---|---|
| 逐节点递归校验 | O(min(n,m)) | O(min(h1,h2)) | 无 | 不匹配立刻终止,效率最优 |
| 遍历(中/后序)对比 | O(n+m) | O(n+m) | 遍历序列相同≠树结构相同 | 需完整遍历 + 额外存储,效率低 |
关键缺陷示例 :树 A(根 1→左 2)和树 B(根 2→右 1)的中序遍历结果都是 [2,1],但实际结构不同,遍历对比会误判。
3、标准模板代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
// 1. 递归终止锚点:两棵树同步走到边界,结构匹配
if (p == null && q == null) return true;
// 2. 结构不匹配:一个空一个非空(已排除都空的情况)
if (p == null || q == null) return false;
// 3. 值不匹配:结构合法后再校验值,避免空指针
if (p.val != q.val) return false;
// 4. 同步递归校验子树:左子树和右子树必须都匹配(短路特性提前终止)
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
代码关键细节
- 终止条件顺序:必须先判断 "都空",再判断 "一空一非空",逻辑不可逆;
- 空指针防护 :先校验结构(是否为空),再访问
val,是新手避坑核心; - 短路特性 :
&&左边为false时,右边递归不会执行,大幅减少无效遍历。
4、典型场景验证
场景 1:两棵树完全相同
树p: 树q:
1 1
/ \ / \
2 3 2 3
- 执行流程:根节点匹配 → 左子节点 2 匹配 → 右子节点 3 匹配 → 返回
true。
场景 2:结构相同但值不同
树p: 树q:
1 1
/ \ / \
2 3 2 4
- 执行流程:根节点匹配 → 左子节点 2 匹配 → 右子节点 3≠4 → 返回
false(提前终止,无需遍历其他节点)。
场景 3:结构不同
树p: 树q:
1 1
/ \
2 2
- 执行流程:根节点匹配 → 校验左子树(p.left=2,q.left=null)→ 结构不匹配 → 返回
false。
5、面试答题话术
我会用逐节点递归同步校验的方法解决这个问题,核心思路是:
- 以 "两个节点都为空" 作为递归终止锚点(树的天然边界,避免无限递归);
- 先校验结构(是否为空)再校验值(避免空指针),最后同步递归校验左右子树;
- 这个方法比 "遍历存结果再对比" 更优 ------ 利用短路特性提前终止,时间 / 空间复杂度更低,且能避免 "遍历序列相同但结构不同" 的误判。
总结
- 核心逻辑:递归终止锚点(都空返回 true)+ 先结构后值校验 + 同步子树校验,是最优解;
- 效率关键 :
&&短路特性,不匹配立刻终止,无需遍历全部节点; - 避坑点:终止条件顺序不可调换,必须先判结构再判值,避免空指针异常。
三、另一棵树的子树
1、题目核心定义
- 问题本质 :判断给定的
subRoot树是否是root树的子树 (子树定义:从root树的某个节点开始,该节点及其所有后代节点构成的树与subRoot完全一致)。 - 输入输出 :输入两棵二叉树的根节点
root和subRoot,输出布尔值(true表示是子树,false表示不是);边界场景:若subRoot为空树,直接返回true(空树是所有树的子树);若root为空且subRoot非空,返回false。
2、核心解法逻辑(递归)
整体思路
通过「遍历找候选根 + 校验子树一致性」两步实现:
isSubtree方法:递归遍历root树的所有节点,将每个节点作为subRoot的 "候选根";isSameTree方法:校验某个 "候选根" 对应的子树是否与subRoot完全一致。
1. 核心方法:isSubtree(遍历找候选根)
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
// 终止条件1:subRoot是空树,直接返回true(空树是所有树的子树)
if (subRoot == null) return true;
// 终止条件2:root是空树(且subRoot非空),返回false
if (root == null) return false;
// 递归逻辑:先找左子树的候选根 → 再找右子树的候选根 → 校验当前节点是否是匹配根
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot) || isSameTree(root, subRoot);
}
- 执行流程 :
- 先递归 "递" 到
root树的最深处(直到触达null节点); - 再从最深处 "归" 回来,逐个节点验证是否是匹配根;
||是短路或:只要找到一个匹配的候选根,立即返回true,无需遍历剩余节点。
- 先递归 "递" 到
2. 辅助方法:isSameTree(校验子树一致性)
public boolean isSameTree(TreeNode root, TreeNode subRoot) {
// 终止条件1:两者都为空,说明匹配
if (root == null && subRoot == null) return true;
// 终止条件2:其中一个为空、另一个非空,说明不匹配
if (root == null || subRoot == null) return false;
// 终止条件3:当前节点值不相等,说明不匹配
if (root.val != subRoot.val) return false;
// 递归逻辑:当前节点匹配后,需同时校验左、右子树都匹配
return isSameTree(root.left, subRoot.left) && isSameTree(root.right, subRoot.right);
}
- 执行流程 :
- 先校验当前节点是否匹配(值相等 + 非空状态一致);
- 再递归校验左子树、右子树是否都匹配;
&&保证:只有 "根 + 左 + 右" 全匹配,才返回true。
3、关键细节解析
- 递归的 "递" 与 "归" :
- "递":
isSubtree会先遍历到root树的叶子节点(如节点 4),此时isSubtree(4, 2)内部三个条件都返回false,最终给父节点(节点 2)返回false; - "归":回到父节点(节点 2)后,触发
isSameTree(2, 2),校验节点 2 及其子树是否与subRoot完全一致,匹配成功则返回true。
- "递":
- 终止条件的作用 :
subRoot == null:仅处理 "subRoot 是空树" 这一种场景,且只在入口层生效(递归中 subRoot 参数不变);root == null:是递归的 "刹车",避免无限递归,也是判断 "空树无法包含非空子树" 的核心。
- 方法分工 :
isSubtree:只负责 "找候选根",不关心子树是否匹配;isSameTree:只负责 "校验匹配",不负责遍历节点。
4、复杂度分析
- 时间复杂度 :
O(n × m),其中n是root树的节点数,m是subRoot树的节点数。每个root节点都可能触发一次isSameTree校验(最多n次),每次校验最多遍历m个节点。 - 空间复杂度 :
O(max(h1, h2)),h1是root树的高度,h2是subRoot树的高度。递归调用栈的深度取决于两棵树的最大高度,最坏情况(链状树)为O(n)或O(m)。
5、核心结论
- 解题核心是「遍历 + 校验」:先通过
isSubtree遍历所有节点找候选根,再通过isSameTree校验子树一致性; - 递归的关键是 "先递后归":递到最深处排除错误节点,归到父节点验证正确节点;
- 短路逻辑(
||/&&)是性能优化的关键,避免无效遍历和校验。