【LeetCode 热题 100】(八)二叉树
94. 二叉树的中序遍历
/*** 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 List<Integer> inorderTraversal(TreeNode root) {List<Integer> list = new LinkedList<>();zhongxu(root, list);return list;}public static void zhongxu(TreeNode root, List<Integer> list){if(root == null){return;}zhongxu(root.left, list);list.add(root.val);zhongxu(root.right, list);}}
这段代码实现了二叉树的中序遍历(Inorder Traversal),采用递归方法解题。以下是分步解题思路描述:
1. 理解中序遍历要求
- 中序遍历顺序:左子树 → 根节点 → 右子树
- 目标:按此顺序输出所有节点的值
2. 设计递归函数
- 终止条件:当前节点为空时直接返回(无需操作)
- 递归逻辑(分三步):
- 遍历左子树:递归处理当前节点的左子树
- 访问根节点:将当前节点的值加入结果列表
- 遍历右子树:递归处理当前节点的右子树
3. 代码执行流程
- 初始化:创建空列表
list
存储结果 - 启动递归:从根节点
root
开始递归 - 递归过程示例(以二叉树
[1, null, 2, 3]
为例):- 访问根节点
1
:- 递归左子树(
1.left
为null
→ 返回) - 添加根节点值
list.add(1)
- 递归右子树(节点
2
)
- 递归左子树(
- 访问节点
2
:- 递归左子树(节点
3
) - 添加节点值
list.add(2)
(暂不执行,先处理左子树)
- 递归左子树(节点
- 访问节点
3
:- 递归左子树(
null
→ 返回) - 添加节点值
list.add(3)
- 递归右子树(
null
→ 返回)
- 递归左子树(
- 回溯到节点
2
:- 添加
2
→list = [1, 3, 2]
- 递归右子树(
null
→ 返回)
- 添加
- 访问根节点
- 返回结果:最终列表
[1, 3, 2]
4. 关键特点
- 时间复杂度 O(n):每个节点访问一次
- 空间复杂度 O(h):递归栈深度 = 树高度(最坏情况 O(n))
- 递归本质:利用函数调用栈隐式保存状态,符合中序遍历的深度优先搜索(DFS)特性
5. 与遍历顺序的对应关系
zhongxu(root.left, list); // 左子树
list.add(root.val); // 根节点
zhongxu(root.right, list); // 右子树
总结:代码通过递归函数严格遵循中序遍历定义,以简洁的方式完成“左→根→右”的访问顺序,最终返回有序节点值列表。
104. 二叉树的最大深度
/*** 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 int maxDepth(TreeNode root) {int max_height = getHeight(root);return max_height;}public static int getHeight(TreeNode root){if(root == null){return 0;}int left_height = getHeight(root.left);int right_hieght = getHeight(root.right);int max_height = Math.max(left_height, right_hieght) + 1;return max_height;}}
解题思路:计算二叉树的最大深度(高度)
核心问题
计算二叉树从根节点到最远叶子节点的最长路径上的节点数(即树的高度)。
关键性质
二叉树的最大深度 = 左右子树的最大深度 + 1(当前节点自身高度)
递归解法步骤
-
终止条件(递归基):
- 当前节点为空(
null
)时,深度为0(空树高度为0)
- 当前节点为空(
-
递归分解:
- 计算左子树的最大深度:
left_height = getHeight(root.left)
- 计算右子树的最大深度:
right_height = getHeight(root.right)
- 注意:变量名拼写应为
right_height
(代码中为right_hieght
,不影响执行但建议修正)
- 计算左子树的最大深度:
-
合并结果:
- 当前树深度 =
max(左子树深度, 右子树深度) + 1
+1
表示当前节点自身的高度贡献
- 当前树深度 =
执行流程示例(以二叉树 [3,9,20,null,null,15,7] 为例)
3/ \9 20/ \15 7
-
访问根节点
3
:- 左子树深度:
getHeight(9)
→ 1 - 右子树深度:
getHeight(20)
→ 2 - 结果:
max(1, 2) + 1 = 3
- 左子树深度:
-
访问节点
9
(叶子节点):- 左右子树均为
null
→ 返回max(0,0) + 1 = 1
- 左右子树均为
-
访问节点
20
:- 左子树深度:
getHeight(15)
→ 1 - 右子树深度:
getHeight(7)
→ 1 - 结果:
max(1,1) + 1 = 2
- 左子树深度:
-
叶子节点
15
/7
处理:- 左右子树均为
null
→ 返回1
- 左右子树均为
复杂度分析
- 时间复杂度 O(n):每个节点访问一次
- 空间复杂度 O(h):递归栈深度 = 树高度(最坏情况 O(n) 当树退化为链表时)
代码结构解析
public int maxDepth(TreeNode root) {return getHeight(root); // 入口直接调用递归函数
}private int getHeight(TreeNode root) {if (root == null) return 0; // 终止条件int left = getHeight(root.left); // 递归左子树int right = getHeight(root.right); // 递归右子树return Math.max(left, right) + 1; // 合并结果
}
重要结论
- 叶子节点高度为1:符合 “节点到自身” 的深度定义
- 递归方向:自底向上计算(从叶子节点向上累加高度)
- 核心公式:
树高 = max(左子树高, 右子树高) + 1
该解法利用二叉树天然的递归结构,通过深度优先搜索(DFS)实现高效的高度计算,是解决树深度问题的经典范式。
226. 翻转二叉树
/*** 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 TreeNode invertTree(TreeNode root) {return reverse(root);}public static TreeNode reverse(TreeNode root){if(root == null){return null;}TreeNode root_left = reverse(root.left);TreeNode root_right = reverse(root.right);root.right = root_left;root.left = root_right;return root;}
}
解题思路:翻转二叉树(Invert Binary Tree)
核心问题
将二叉树中每个节点的左右子树进行交换,形成镜像对称的树结构。
关键策略
- 递归分解:将整棵树的翻转分解为"翻转左子树 + 翻转右子树 + 交换左右子树"
- 自底向上:先处理叶子节点,再回溯处理上层节点
递归解法步骤
-
终止条件(递归基):
- 当前节点为空(
null
)时直接返回null
(空树无需翻转)
- 当前节点为空(
-
递归分解:
- 翻转左子树:
root_left = reverse(root.left)
- 翻转右子树:
root_right = reverse(root.right)
- 翻转左子树:
-
交换子树:
- 当前节点的左指针指向翻转后的右子树:
root.left = root_right
- 当前节点的右指针指向翻转后的左子树:
root.right = root_left
- 当前节点的左指针指向翻转后的右子树:
-
返回结果:
- 返回已翻转的当前节点作为子树的新根节点
执行流程示例(以二叉树 [4,2,7,1,3,6,9] 为例)
原始树:4/ \2 7/ \ / \
1 3 6 9翻转过程:
1. 递归到叶子节点 1:左右子树为null → 返回自身
2. 递归到叶子节点 3:左右子树为null → 返回自身
3. 节点 2:- 左子树翻转后 = 1- 右子树翻转后 = 3- 交换:左=3, 右=1
4. 同理处理节点 6/9 → 节点 7 交换为左=9, 右=6
5. 根节点 4:- 左子树翻转后 = 2(已变成3←2→1)- 右子树翻转后 = 7(已变成9←7→6)- 交换:左=7(9←7→6), 右=2(3←2→1)结果树:4/ \7 2/ \ / \
9 6 3 1
代码结构解析
public TreeNode invertTree(TreeNode root) {return reverse(root); // 入口调用递归函数
}private TreeNode reverse(TreeNode root) {if (root == null) return null; // 终止条件TreeNode leftFlipped = reverse(root.left); // 翻转左子树TreeNode rightFlipped = reverse(root.right); // 翻转右子树// 交换左右子树root.left = rightFlipped; root.right = leftFlipped;return root; // 返回当前根节点
}
关键特点
-
后序遍历框架:
- 先递归处理左右子树(后序遍历顺序)
- 再处理当前节点(交换子树)
-
原地修改:
- 直接在原树上修改指针指向,不创建新节点
- 空间复杂度 O(1)(不计递归栈空间)
-
复杂度分析:
- 时间复杂度 O(n):每个节点访问一次
- 空间复杂度 O(h):递归栈深度 = 树高度(最坏情况 O(n))
思维拓展
- 前序实现方案:先交换当前节点的左右子树,再递归翻转左右子树
- 迭代实现方案:使用队列进行层序遍历,每访问一个节点立即交换其左右子树
总结:该解法利用二叉树递归特性,通过"解决子问题+合并结果"的思想,以简洁优雅的方式实现树结构的翻转,是分治策略(Divide and Conquer)在树问题中的典型应用。
101. 对称二叉树
/*** 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 isSymmetric(TreeNode root) {return check(root.left, root.right);}public boolean check(TreeNode lChild, TreeNode rChild){if(lChild==null && rChild==null){return true;}if(lChild==null || rChild==null){return false;}return lChild.val==rChild.val&&check(lChild.left,rChild.right)&&check(lChild.right,rChild.left);}
}
解题思路:判断二叉树是否对称(镜像对称)
核心问题
验证二叉树是否关于根节点镜像对称,即左子树与右子树互为镜像。
关键策略
- 双指针递归:同时遍历左右子树,比较镜像位置的节点
- 镜像规则:
- 左子树的左节点 ↔ 右子树的右节点
- 左子树的右节点 ↔ 右子树的左节点
递归解法步骤
-
终止条件:
- 两节点均为空 → 对称(
true
) - 仅一节点为空 → 不对称(
false
)
- 两节点均为空 → 对称(
-
节点值校验:
- 当两节点都不为空时,首先比较节点值是否相等
-
递归镜像校验:
- 检查外侧对称:
左子树的左节点 vs 右子树的右节点
- 检查内侧对称:
左子树的右节点 vs 右子树的左节点
- 检查外侧对称:
-
结果合并:
- 当前节点值相等 + 外侧对称 + 内侧对称 → 返回
true
- 当前节点值相等 + 外侧对称 + 内侧对称 → 返回
执行流程示例(以对称树 [1,2,2,3,4,4,3] 为例)
1/ \2 2 // 根节点比较:2==2/ \ / \
3 4 4 3步骤:
1. 比较根节点左右子节点 (2 vs 2)- 值相等 → 继续
2. 检查外侧:左2的左3 vs 右2的右3 → 递归- 值相等,且都是叶子节点 → 返回 true
3. 检查内侧:左2的右4 vs 右2的左4 → 递归- 值相等,且都是叶子节点 → 返回 true
4. 合并结果:true && true && true → 最终 true
非对称情况处理(以 [1,2,2,null,3,null,3] 为例)
1/ \2 2 // 值相等/ \ / \3 3 // 左2无左节点,右2有右节点 → 结构不对称步骤:
1. 比较根节点左右子节点 (2 vs 2) → 值相等
2. 检查外侧:左2的左节点(null) vs 右2的右节点(3)- 一空一非空 → 立即返回 false
代码结构解析
public boolean isSymmetric(TreeNode root) {// 从根节点的左右子树开始比较return check(root.left, root.right);
}private boolean check(TreeNode left, TreeNode right) {// 终止条件1:两节点均为空if (left == null && right == null) return true;// 终止条件2:一空一非空if (left == null || right == null) return false;// 节点值校验 + 递归镜像检查return left.val == right.val && check(left.left, right.right) // 外侧对称检查&& check(left.right, right.left); // 内侧对称检查
}
关键特点
-
双节点同步递归:
- 同时遍历左右子树镜像位置节点
- 递归路径呈镜像对称(左→左 vs 右→右,左→右 vs 右→左)
-
短路优化:
- 利用
&&
的短路特性:任一条件失败立即终止递归 - 提升非对称树的检测效率
- 利用
-
复杂度分析:
- 时间复杂度 O(n):每个节点访问一次
- 空间复杂度 O(h):递归栈深度 = 树高度(最坏情况 O(n))
思维拓展
- 迭代实现:使用队列同时存入镜像位置节点对,循环比较
- 对称本质:树的前序遍历(左→右)与对称前序遍历(右→左)序列相同
总结:该解法通过精妙的双指针递归,严格遵循镜像对称的定义(结构对称 + 值对称),以高效简洁的方式解决了二叉树对称性验证问题,是理解树递归结构的经典案例。
543. 二叉树的直径
/*** 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 {int ans;public int diameterOfBinaryTree(TreeNode root) {ans = 1;depth(root);return ans - 1;}public int depth(TreeNode node){if(node == null){return 0;}int L = depth(node.left);int R = depth(node.right);ans = Math.max(ans, L+R+1);return Math.max(L,R) + 1;}}
解题思路:计算二叉树的直径(最长路径长度)
核心问题
二叉树的直径定义为任意两个节点间最长路径的边数(注意不是节点数)。关键观察:
- 最长路径不一定经过根节点
- 直径 = 某节点左子树深度 + 右子树深度(该节点为路径最高点)
解决策略
- 深度优先搜索(DFS):递归计算子树深度
- 全局跟踪最大值:实时更新可能的最大直径
- 深度与直径的关系:
- 节点深度 = max(左子树深度, 右子树深度) + 1
- 当前节点为根的路径长度 = 左深度 + 右深度 + 1(节点数)
- 直径 = 最大路径节点数 - 1(边数)
算法步骤
-
初始化:
- 全局变量
ans
记录最大路径节点数(初始为1,单个节点时路径节点数为1)
- 全局变量
-
递归计算深度:
public int depth(TreeNode node) {if (node == null) return 0; // 空子树深度为0int L = depth(node.left); // 递归左子树深度int R = depth(node.right); // 递归右子树深度ans = Math.max(ans, L + R + 1); // 更新最大路径节点数return Math.max(L, R) + 1; // 返回当前子树深度 }
-
结果转换:
- 最终直径 =
ans - 1
(将节点数转为边数)
- 最终直径 =
执行流程示例(以二叉树 [1,2,3,4,5] 为例)
1/ \2 3/ \4 5
-
计算节点4深度:
- L=0, R=0 → 路径节点数=1 → ans=max(1,1)=1 → 返回深度1
-
计算节点5深度:
- 同上 → 返回深度1
-
计算节点2深度:
- L=1(节点4深度), R=1(节点5深度)
- 路径节点数=1+1+1=3 → ans=max(1,3)=3
- 返回深度 max(1,1)+1=2
-
计算节点3深度:
- L=0, R=0 → 路径节点数=1 → ans不变
- 返回深度1
-
计算根节点1深度:
- L=2(节点2深度), R=1(节点3深度)
- 路径节点数=2+1+1=4 → ans=max(3,4)=4
- 返回深度 max(2,1)+1=3
-
最终结果:直径 = 4 - 1 = 3(路径[4,2,1,3]或[5,2,1,3])
关键点解析
-
后序遍历框架:
- 先递归获取左右子树信息
- 再处理当前节点(计算路径+更新最大值)
-
全局变量
ans
的作用:- 记录遍历过程中出现的最大路径节点数
- 覆盖所有可能的路径(包括不经过根节点的路径)
-
深度计算与直径更新的关系:
- 每个节点返回自己的深度(给父节点使用)
- 同时用
L+R+1
计算以当前节点为顶点的路径
-
复杂度分析:
- 时间复杂度:O(n) - 每个节点访问一次
- 空间复杂度:O(h) - 递归栈深度(树高度)
特殊处理说明
- 叶子节点:深度=1,路径节点数=1(L=0,R=0 → 0+0+1=1)
- 单边树:ans 始终保持最大值(如左子树深度5,右子树深度0 → 路径节点数=5+0+1=6)
- 空树处理:depth()返回0,ans保持1 → 直径=0(符合定义)
总结:该解法通过巧妙的深度优先搜索,在计算子树深度的同时动态更新全局最大路径值,以O(n)时间复杂度高效解决了二叉树直径问题,体现了"在递归中求解,在回溯中更新"的经典树遍历思想。
102. 二叉树的层序遍历
/*** 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 List<List<Integer>> levelOrder(TreeNode root) {Queue<TreeNode> queue = new LinkedList<>();List<List<Integer>> list1 = new LinkedList<>();if(root==null){return list1;}queue.offer(root);while(!queue.isEmpty()){List<Integer> list = new LinkedList<>();int size = queue.size();for(int i=1; i<=size; i++){TreeNode node = queue.poll();list.add(node.val);if(node.left != null){queue.offer(node.left);}if(node.right != null){queue.offer(node.right);}}list1.add(list);}return list1;}
}
解题思路:二叉树的层序遍历(BFS实现)
核心问题
按层级顺序遍历二叉树,将每层节点值存入独立子列表,最终返回层级结构的结果列表。
关键策略
- 广度优先搜索(BFS):使用队列实现层级遍历
- 层级分离技巧:通过固定循环次数区分不同层级
- 队列操作:
- 入队:子节点入队时即进入下一层
- 出队:处理当前层节点
算法步骤
-
初始化:
- 创建队列(
LinkedList
实现) - 创建结果列表(嵌套列表结构)
- 处理空树特殊情况:直接返回空列表
- 创建队列(
-
启动遍历:
- 根节点入队:
queue.offer(root)
- 根节点入队:
-
层级循环(当队列非空时):
while(!queue.isEmpty()) {// 步骤3.1:创建当前层列表List<Integer> level = new LinkedList<>();// 步骤3.2:获取当前层节点数int levelSize = queue.size();// 步骤3.3:遍历当前层所有节点for(int i=0; i<levelSize; i++) {// 出队节点TreeNode node = queue.poll();// 节点值加入当前层列表level.add(node.val);// 子节点入队(即下一层节点)if(node.left != null) queue.offer(node.left);if(node.right != null) queue.offer(node.right);}// 步骤3.4:完成当前层遍历result.add(level); }
执行流程示例(二叉树 [3,9,20,null,null,15,7])
3/ \9 20/ \15 7
-
初始状态:
- 队列:
[3]
- 结果:
[]
- 队列:
-
第一层处理:
- 当前层大小:1
- 出队节点:3
- 加入列表:
[3]
- 子节点入队:9→20(队列变为
[9,20]
)
- 加入列表:
- 完成层:结果=
[[3]]
-
第二层处理:
- 当前层大小:2
- 出队节点:9
- 加入列表:
[9]
- 无子节点(队列变为
[20]
)
- 加入列表:
- 出队节点:20
- 加入列表:
[9,20]
- 子节点入队:15→7(队列变为
[15,7]
)
- 加入列表:
- 完成层:结果=
[[3],[9,20]]
-
第三层处理:
- 当前层大小:2
- 出队节点:15→7
- 加入列表:
[15,7]
- 无子节点
- 加入列表:
- 完成层:结果=
[[3],[9,20],[15,7]]
关键特点解析
-
层级分离核心:
levelSize = queue.size()
锁定当前层节点数- 后续入队的子节点不影响当前层循环次数
-
队列动态变化:
- 处理当前层节点时,其子节点同时入队
- 出队顺序保证层级顺序(从左到右)
-
数据结构选择:
- 队列:
LinkedList
(实现Queue
接口) - 结果列表:
LinkedList
(高效尾部插入)
- 队列:
-
复杂度分析:
- 时间复杂度:O(n) - 每个节点访问一次
- 空间复杂度:O(n) - 队列最大存储宽度节点数
与深度优先搜索(DFS)对比
特性 | BFS实现(本题) | DFS实现 |
---|---|---|
遍历顺序 | 层级顺序(横向) | 深度顺序(纵向) |
数据结构 | 队列 | 栈/递归栈 |
层级处理 | 天然支持层级分离 | 需额外记录层级深度 |
适用场景 | 按层处理/最短路径问题 | 深度路径相关问题 |
总结:该解法通过队列的先进先出特性,配合层级大小记录技巧,高效实现了二叉树的层级遍历。算法严格遵循"处理当前层→准备下一层"的迭代过程,是BFS在树结构中的经典应用。
108. 有序数组转换为二叉搜索树
解题思路分析:将有序数组转换为平衡二叉搜索树(BST)
核心问题
给定一个升序排列的整数数组,构造一棵高度平衡的二叉搜索树(BST),要求每个节点的左右子树高度差不超过 1。
关键性质
- BST 中序遍历有序:BST 的中序遍历结果就是升序数组。
- 平衡条件:左右子树节点数量尽可能相等(最多相差 1)。
解题策略:分治法(Divide and Conquer)
-
分治核心:
- 每次选取当前区间的中间元素作为根节点
- 左区间构建左子树,右区间构建右子树
- 递归处理直到区间为空
-
平衡性保障:
- 选择中间元素作为根节点 ⇒ 左右子树节点数差值 ≤ 1
- 数学证明:设区间长度
n
- 左子树节点数:
⌊(n-1)/2⌋
- 右子树节点数:
⌈(n-1)/2⌉
- 差值始终 ≤ 1
- 左子树节点数:
代码解析
class Solution {public TreeNode sortedArrayToBST(int[] nums) {return helper(nums, 0, nums.length - 1); // 入口:处理整个数组}private TreeNode helper(int[] nums, int left, int right) {// 终止条件:区间无效时返回 nullif (left > right) return null;// 分治核心操作 (3 步)int mid = left + (right - left) / 2; // 1. 找中间点(防溢出写法)TreeNode root = new TreeNode(nums[mid]); // 2. 创建根节点// 3. 递归构建子树root.left = helper(nums, left, mid - 1); // 左区间:[left, mid-1]root.right = helper(nums, mid + 1, right); // 右区间:[mid+1, right]return root;}
}
分步图解(示例:[-10, -3, 0, 5, 9]
)
复杂度分析
- 时间复杂度:O(n)
每个节点恰好访问一次,共 n 个节点 - 空间复杂度:O(log n)
递归栈深度 = 树的高度(平衡 BST 的高度为 log n)
技术细节
-
中间点计算:
- 推荐:
mid = left + (right - left) / 2
- 避免整数溢出(比
(left+right)/2
更安全)
- 推荐:
-
终止条件:
left > right
表示当前区间为空- 不可省略,否则会数组越界
-
平衡性数学保证:
# 设区间长度 n = right - left + 1 left_size = (n - 1) // 2 # 左子树节点数 right_size = n - 1 - left_size # 右子树节点数 abs(left_size - right_size) ≤ 1 # 始终成立
为什么这个方法有效?
- BST 性质:左子树 < 根 < 右子树 ⇒ 通过取中间值保证
- 平衡性质:递归时均匀分割数组 ⇒ 子树高度差 ≤ 1
- 最优解:分治法直接对应 BST 的数学定义
关键洞察:有序数组的中间点就是 BST 的根节点,这个性质递归适用于所有子树。
98. 验证二叉搜索树
解题思路分析:验证二叉搜索树(BST)
核心问题
判断给定的二叉树是否是有效的二叉搜索树(BST),需满足:
- 节点的左子树所有节点值 小于 当前节点值
- 节点的右子树所有节点值 大于 当前节点值
- 所有子树自身也必须是 BST
关键挑战
- 不能仅检查单个节点的子节点,需要确保整个子树的值都在特定范围内
- 需要处理整数边界值(如
Integer.MIN_VALUE
和Integer.MAX_VALUE
)
解题策略:上下界递归法
-
核心思想:
- 每个节点都有允许的取值区间
(lower, upper)
- 根节点区间:
(-∞, +∞)
- 递归时动态收缩区间:
- 左子树区间:
(父节点的下界, 父节点值)
- 右子树区间:
(父节点值, 父节点的上界)
- 左子树区间:
- 每个节点都有允许的取值区间
-
边界处理:
- 使用
long
类型避免整数边界问题(如节点值为Integer.MAX_VALUE
) - 区间为开区间(不包含边界值)
- 使用
代码解析
class Solution {public boolean isValidBST(TreeNode root) {// 初始调用:根节点允许范围为整个 long 范围return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);}private boolean isValidBST(TreeNode node, long lower, long upper) {// 终止条件:空节点总是有效的if (node == null) return true;// 检查当前节点是否在允许范围内if (node.val <= lower || node.val >= upper) {return false; // 越界立即返回失败}// 递归检查子树(同时收缩边界):// 左子树:必须小于当前值 → 上界更新为 node.val// 右子树:必须大于当前值 → 下界更新为 node.valreturn isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);}
}
算法流程图示(示例:[5,1,7,null,null,6,8]
)
关键步骤说明
-
根节点检查:
- 5 ∈ (-∞, +∞) → 有效
- 创建左子树边界:(-∞, 5)
- 创建右子树边界:(5, +∞)
-
左子树检查:
- 1 ∈ (-∞, 5) → 有效
- 叶子节点直接返回 true
-
右子树检查:
- 7 ∈ (5, +∞) → 有效
- 左子节点 6 ∈ (5, 7) → 有效
- 右子节点 8 ∈ (7, +∞) → 有效
复杂度分析
- 时间复杂度:O(n)
每个节点仅访问一次 - 空间复杂度:O(h)
递归栈深度 = 树高度(BST 平衡时为 O(log n),最坏链状为 O(n))
边界处理详解
// 使用 long 避免整数边界问题:
Long.MIN_VALUE = -9223372036854775808
Long.MAX_VALUE = 9223372036854775807// 处理节点值为 Integer 边界的情况:
// 示例:节点值 = Integer.MAX_VALUE (2147483647)
isValidBST(node.right, 2147483647, Long.MAX_VALUE) // 正确传递边界
为什么这个方法有效?
-
数学归纳法保证:
- 基础:空树是 BST
- 归纳:若左右子树在收缩后边界内是 BST,则整树是 BST
-
区间收缩模拟 BST 定义:
- 左子树值必须小于根 → 上界 = 根值
- 右子树值必须大于根 → 下界 = 根值
-
提前剪枝优化:
- 当任何节点越界时立即终止递归
- 避免不必要的子树遍历
关键洞察:BST 的本质是节点值存在全局约束条件,通过动态传递边界值,将全局约束转化为局部约束。
230. 二叉搜索树中第k小的元素
解题思路:二叉搜索树中第 K 小的元素(迭代中序遍历法)
核心问题
在二叉搜索树(BST)中找到第 k 小的元素。
关键性质:BST 的中序遍历结果是有序升序序列。
解题策略:迭代中序遍历 + 提前终止
-
利用 BST 中序有序性:
- 中序遍历顺序:左子树 → 根节点 → 右子树
- 遍历顺序即元素升序排列,第 k 个访问的节点即第 k 小元素
-
迭代优势:
- 递归遍历需完成整个遍历,迭代可在找到目标后立即终止
- 显式栈模拟递归过程,避免递归函数调用开销
代码解析
class Solution {public int kthSmallest(TreeNode root, int k) {Deque<TreeNode> stack = new LinkedList<>(); // 显式栈(代替递归栈)int count = 0; // 节点访问计数器// 循环条件:栈未空 或 当前节点未处理完while (!stack.isEmpty() || root != null) {// 深度优先访问左子树(模拟递归左子树)while (root != null) {stack.push(root); // 当前节点入栈root = root.left; // 向最左叶子节点推进}// --- 中序遍历访问点 ---root = stack.pop(); // 弹出当前子树的根节点count++; // 计数器+1(访问顺序即升序排名)// 找到第 k 小元素if (count == k) return root.val;// 转向处理右子树root = root.right;}return 0; // 此处不会执行(题目保证 k 有效)}
}
算法流程图示(示例:[5,3,6,2,4,null,null,1]
, k=3)
分步执行过程
步骤 | 栈状态 | 当前 root | 操作 | count | 说明 |
---|---|---|---|---|---|
1 | [] | 5 | 5 入栈 → 访问左子 3 | 0 | |
2 | [5] | 3 | 3 入栈 → 访问左子 2 | 0 | |
3 | [5,3] | 2 | 2 入栈 → 访问左子 1 | 0 | |
4 | [5,3,2] | 1 | 1 入栈 → 访问左子 null | 0 | |
5 | [5,3,2,1] | null | 弹出 1 → count=1 | 1 | 访问节点 1 |
6 | [5,3,2] | 1.right=null | 弹出 2 → count=2 | 2 | 访问节点 2 |
7 | [5,3] | 2.right=null | 弹出 3 → count=3 | 3 | 返回 3 (k=3 目标达成) |
复杂度分析
- 时间复杂度:O(H + k)
- H 为树高度,到达最左叶节点需 O(H)
- 找到第 k 小元素需遍历 k 个节点
- 空间复杂度:O(H)
- 栈空间占用最大为树高度(BST 平衡时为 O(log n))
关键优势
- 提前终止:找到第 k 小元素后立即返回,无需遍历整棵树
- 避免递归:迭代减少函数调用开销,适合大规模数据
- 内存可控:显式栈空间复杂度 O(H),递归隐式栈为 O(n)
边界处理
- 空树处理:题目保证 k 有效,无需特殊处理
- k 的有效性:题目保证 1 ≤ k ≤ BST 节点总数
- 整型溢出:计数器使用 int(题目节点数 ≤ 10^4)
为什么选择迭代而非递归?
递归中序遍历需遍历整个树才能得到结果,而迭代方法在找到第 k 小元素后可立即终止,效率更高。
199. 二叉树的右视图
解题思路:二叉树的右视图(BFS层序遍历 + 深度映射)
核心问题
给定一棵二叉树,返回从右侧观察树时能看到的节点值(即每层最右侧节点值)。
关键性质
- 右视图定义:每层最右侧节点组成的序列
- 树结构特性:每层节点从左到右排列,最右侧节点即该层最后一个节点
解题策略:广度优先搜索(BFS)层序遍历
-
核心思想:
- 使用队列进行 BFS 层序遍历
- 在遍历每层节点时,最后访问的节点就是该层最右侧节点
- 通过深度映射记录每层最后访问的节点值
-
算法选择原因:
- BFS 天然按层遍历,符合题目分层需求
- 最后访问原则:同一深度下,后访问的节点总是更靠右
代码解析
class Solution {public List<Integer> rightSideView(TreeNode root) {// 边界情况:空树直接返回空列表if (root == null) return new ArrayList<>();int max_depth = -1; // 记录树的最大深度Map<Integer, Integer> depthMap = new HashMap<>(); // 深度->节点值映射Queue<TreeNode> nodeQueue = new LinkedList<>(); // 节点队列Queue<Integer> depthQueue = new LinkedList<>(); // 深度队列// 初始化:根节点入队(深度=0)nodeQueue.add(root);depthQueue.add(0);while (!nodeQueue.isEmpty()) {TreeNode node = nodeQueue.remove();int depth = depthQueue.remove();if (node != null) {max_depth = Math.max(depth, max_depth); // 更新最大深度depthMap.put(depth, node.val); // 更新当前深度的最后节点值// 左子节点入队(深度+1)nodeQueue.add(node.left);depthQueue.add(depth + 1);// 右子节点入队(深度+1)nodeQueue.add(node.right);depthQueue.add(depth + 1);}}// 构建结果:从深度0到max_depth提取节点值List<Integer> result = new ArrayList<>();for (int depth = 0; depth <= max_depth; depth++) {result.add(depthMap.get(depth));}return result;}
}
算法流程图示(示例:[1,2,3,null,5,null,4]
)
关键步骤说明
-
初始化:
- 根节点1入队(深度0)
depthMap
初始为空,max_depth=-1
-
遍历过程:
当前节点 深度 操作 depthMap更新 max_depth 1 0 左子2/右子3入队 {0:1}
0 2 1 左子null/右子5入队 {0:1, 1:2}
1 3 1 左子null/右子4入队 {0:1, 1:3}
(覆盖)1 null 2 跳过 - - 5 2 子节点入队 {0:1, 1:3, 2:5}
2 null 2 跳过 - - 4 2 子节点入队 {0:1, 1:3, 2:4}
(覆盖)2 -
结果构建:
- 深度0 → 值1
- 深度1 → 值3
- 深度2 → 值4
- 结果:
[1, 3, 4]
复杂度分析
- 时间复杂度:O(n)
每个节点入队/出队各一次 - 空间复杂度:O(n)
队列存储空间 + 深度映射空间(最坏情况完美二叉树最后一层 O(n))
优化点
-
空节点优化:
// 入队前判断子节点非空 if (node.left != null) {nodeQueue.add(node.left);depthQueue.add(depth + 1); }
避免空节点入队,减少约 50% 队列操作
-
单队列优化:
使用Pair<TreeNode, Integer>
合并节点和深度队列
算法核心逻辑
关键洞察:利用 BFS 层序遍历特性,通过后访问覆盖原则,自然捕获每层最右节点。深度映射表在遍历过程中动态记录每层最后出现的节点值,最终按深度顺序输出即为右视图。
114. 二叉树展开为链表
解题思路描述
这段代码的目标是将二叉树展开为一个单链表(按照先序遍历的顺序),展开后的链表使用 TreeNode
表示,其中每个节点的右指针指向链表中的下一个节点,左指针始终为 null
。代码的实现分为两个主要步骤:
-
先序遍历收集节点:
- 使用递归的先序遍历(根 → 左 → 右)访问二叉树的所有节点。
- 将访问到的节点按顺序存储在一个动态数组(
ArrayList
)中。这个列表记录了节点在先序遍历中的顺序。
-
重新连接节点形成链表:
- 遍历存储节点的列表(从第一个节点到倒数第二个节点)。
- 对于每个节点
pre
(当前节点)和它的下一个节点curr
(列表中的后继节点):- 将
pre
的左指针置为null
(因为链表中不需要左子树)。 - 将
pre
的右指针指向curr
(形成单向链表)。
- 将
- 列表的最后一个节点不需要额外处理,因为它作为叶子节点,其左右指针原本就是
null
,符合链表尾节点的要求。
关键点说明
- 先序遍历的作用:确保节点按根、左子树、右子树的顺序被收集,这正是展开后链表的顺序。
- 连接节点的逻辑:
- 只处理列表中前 (n-1) 个节点((n) 是节点总数),每个节点的右指针指向它的后继节点。
- 最后一个节点保持右指针为
null
(链表结束标志),且其左指针原本就是null
(叶子节点特性),无需额外操作。
- 空间复杂度:(O(n)),用于存储所有节点的列表((n) 是节点数)。
- 时间复杂度:(O(n)),先序遍历和连接节点各遍历一次所有节点。
代码逐行解释
class Solution {public void flatten(TreeNode root) {// 存储先序遍历的节点List<TreeNode> list = new ArrayList<TreeNode>();pre(root, list); // 递归先序遍历,填充列表int size = list.size();// 遍历列表,重新连接节点(从第一个节点到倒数第二个节点)for (int i = 1; i < size; i++) {TreeNode pre = list.get(i - 1); // 当前节点TreeNode curr = list.get(i); // 下一个节点pre.left = null; // 左指针置空pre.right = curr; // 右指针指向下一个节点}// 最后一个节点无需处理:左指针为null(叶子节点),右指针为null(链表结尾)}// 先序遍历的递归函数public void pre(TreeNode root, List<TreeNode> list) {if (root != null) {list.add(root); // 访问根节点pre(root.left, list); // 递归左子树pre(root.right, list); // 递归右子树}}
}
示例说明
假设输入二叉树为:
1/ \2 5/ \ \
3 4 6
- 先序遍历顺序:
[1, 2, 3, 4, 5, 6]
。 - 连接过程:
- 节点
1
→ 右指针指向2
(左指针置null
)。 - 节点
2
→ 右指针指向3
(左指针置null
)。 - 节点
3
→ 右指针指向4
(左指针置null
)。 - 节点
4
→ 右指针指向5
(左指针置null
)。 - 节点
5
→ 右指针指向6
(左指针置null
)。 - 节点
6
→ 左右指针保持null
(链表结束)。
- 节点
- 展开后的链表:
1 → 2 → 3 → 4 → 5 → 6 → null
。
总结
这段代码通过先序遍历收集节点,再线性连接节点,简洁高效地实现了二叉树的展开。它利用了叶子节点的特性(左右指针为 null
),确保链表结尾的正确性,适合所有二叉树情况(包括空树和单节点树)。
105. 从前序和中序遍历序列构建二叉树
解题思路描述
这段代码实现了根据二叉树的前序遍历(preorder)和中序遍历(inorder)序列重建二叉树的功能。核心思路是利用分治法和哈希映射来高效定位根节点并划分左右子树。以下是详细的解题步骤:
核心步骤解析
-
建立中序遍历索引映射
- 使用哈希表存储中序遍历序列中每个值对应的索引位置,便于快速查找根节点位置
- 时间复杂度:O(n),空间复杂度:O(n)
-
递归构建二叉树
- 分治思想:将大问题(构建整棵树)分解为小问题(构建左右子树)
- 关键操作:
- 前序遍历的首元素即为当前子树的根节点
- 通过哈希表快速找到根节点在中序遍历中的位置
- 根据根节点位置划分左右子树区间
-
递归终止条件
- 当前子树的前序遍历区间为空时(
pre_left > pre_right
),返回 null
- 当前子树的前序遍历区间为空时(
递归构建过程详解
假设当前子树在前序遍历的区间为 [pre_left, pre_right]
,在中序遍历的区间为 [in_left, in_right]
:
-
确定根节点:
int rootVal = preorder[pre_left]; // 前序首元素即根节点 int in_root = indexMap.get(rootVal); // 中序遍历中根节点的位置
-
计算左子树节点数:
int leftSize = in_root - in_left; // 中序遍历中根节点左侧元素个数
- 为什么能确定左子树大小?
中序遍历中,根节点左侧全为左子树节点,右侧全为右子树节点
- 为什么能确定左子树大小?
-
划分左右子树区间:
- 左子树:
- 前序区间:
[pre_left + 1, pre_left + leftSize]
(紧接根节点后的 leftSize 个元素) - 中序区间:
[in_left, in_root - 1]
- 前序区间:
- 右子树:
- 前序区间:
[pre_left + leftSize + 1, pre_right]
(左子树之后的所有元素) - 中序区间:
[in_root + 1, in_right]
- 前序区间:
- 左子树:
-
递归构建:
root.left = build(preorder, inorder, pre_left+1, pre_left+leftSize, in_left, in_root-1); root.right = build(preorder, inorder, pre_left+leftSize+1, pre_right, in_root+1, in_right);
示例推演
给定:
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
构建过程:
- 根节点 = 3(preorder[0])
- 在inorder中定位3 → 索引=1
- 左子树大小 = 1 - 0 = 1
- 左子树:
- 前序区间:[1, 1] → 9
- 中序区间:[0, 0] → 9
- 右子树:
- 前序区间:[2, 4] → [20,15,7]
- 中序区间:[2, 4] → [15,20,7]
- 递归构建右子树(20为根,15在左,7在右)
最终二叉树:
3/ \9 20/ \15 7
代码逐行解析
class Solution {private Map<Integer, Integer> indexMap; // 存储中序遍历的索引映射public TreeNode buildTree(int[] preorder, int[] inorder) {int n = preorder.length;indexMap = new HashMap<>();// 建立中序遍历的值->索引映射for (int i = 0; i < n; i++) {indexMap.put(inorder[i], i);}// 启动递归构建,初始区间为整个数组return build(preorder, inorder, 0, n-1, 0, n-1);}private TreeNode build(int[] preorder, int[] inorder,int pre_left, int pre_right, int in_left, int in_right) {if (pre_left > pre_right) return null; // 终止条件int rootVal = preorder[pre_left]; // 前序首元素为根int in_root = indexMap.get(rootVal); // 根节点在中序的位置TreeNode root = new TreeNode(rootVal); // 创建当前根节点int leftSize = in_root - in_left; // 计算左子树节点数// 递归构建左子树(前序:根后连续leftSize个元素)root.left = build(preorder, inorder, pre_left + 1, // 左子树前序起始pre_left + leftSize, // 左子树前序结束in_left, // 左子树中序起始in_root - 1); // 左子树中序结束// 递归构建右子树(前序:左子树之后的所有元素)root.right = build(preorder, inorder, pre_left + leftSize + 1, // 右子树前序起始pre_right, // 右子树前序结束in_root + 1, // 右子树中序起始in_right); // 右子树中序结束return root;}
}
算法特性
- 时间复杂度:O(n)
每个节点仅被处理一次,哈希表构建 O(n),递归构建 O(n) - 空间复杂度:O(n)
哈希表 O(n),递归栈深度最坏 O(n)(当树退化为链表时) - 关键优势:
通过哈希表将中序遍历的根节点查找操作优化到 O(1),避免每次递归的线性查找
适用场景
- 二叉树序列化/反序列化
- 根据遍历结果重建二叉树
- 理解二叉树遍历的递归性质
该算法完美诠释了分治思想在树结构中的应用,通过前序确定根节点,中序确定左右子树边界,是解决二叉树重建问题的经典方法。
437. 路径总和3
解题思路描述
这段代码用于解决二叉树路径和问题:统计二叉树中所有路径和等于目标值 targetSum
的路径数量。路径不需要从根节点开始,也不需要在叶子节点结束,但必须是从父节点指向子节点的连续路径(只能向下)。
核心思路:双重递归遍历
采用双重递归结构,分别处理两种不同路径起点的情况:
- 以当前节点为起点的路径(通过
rootSum
函数处理) - 不以当前节点为起点的路径(通过主函数
pathSum
递归处理)
算法步骤详解
-
主函数
pathSum
(处理所有可能的起点):- 边界处理:如果当前节点为空,返回 0(无路径)
- 统计以当前节点为起点的路径:调用
rootSum(root, targetSum)
- 递归处理左子树:
pathSum(root.left, targetSum)
(统计以左子树节点为起点的路径) - 递归处理右子树:
pathSum(root.right, targetSum)
(统计以右子树节点为起点的路径) - 返回三部分结果的总和
-
辅助函数
rootSum
(处理以当前节点为起点的所有路径):- 边界处理:如果当前节点为空,返回 0
- 初始化计数器
ret = 0
- 检查当前节点值是否满足目标:
- 如果
root.val == targetSum
,计数器 +1(单节点路径)
- 如果
- 递归处理左子树:
- 新目标值 =
targetSum - root.val
- 递归调用
rootSum(root.left, 新目标值)
- 新目标值 =
- 递归处理右子树:
- 新目标值 =
targetSum - root.val
- 递归调用
rootSum(root.right, 新目标值)
- 新目标值 =
- 返回所有满足条件的路径总数
关键特点
- 双重递归结构:
- 外层递归(
pathSum
)遍历树中所有节点作为路径起点 - 内层递归(
rootSum
)遍历从起点向下的所有路径终点
- 外层递归(
- 路径和动态计算:
- 在
rootSum
中,每次递归时更新剩余目标值(targetSum - 当前节点值
) - 当剩余目标值恰好为 0 时找到有效路径
- 在
示例分析
假设二叉树如下:
10/ \5 -3/ \ \3 2 11/ \ \
3 -2 1
目标值 targetSum = 8
-
以根节点 (10) 为起点:
rootSum(10, 8)
→ 10 ≠ 8- 递归左子树:
rootSum(5, -2)
→ 无解 - 递归右子树:
rootSum(-3, -2)
→ 无解 - 结果:0
-
以节点 5 为起点:
rootSum(5, 8)
:- 路径
5→3
:5+3=8 → 有效 - 路径
5→2→1
:5+2+1=8 → 有效
- 路径
- 结果:2
-
以节点 -3 为起点:
rootSum(-3, 8)
:- 路径
-3→11
:-3+11=8 → 有效
- 路径
- 结果:1
-
其他节点:均无有效路径
最终结果:0 (10) + 2 (5) + 1 (-3) = 3
复杂度分析
- 时间复杂度:O(n²)
- 最坏情况(链表状树):每个节点调用
rootSum
耗时 O(n),共 n 个节点
- 最坏情况(链表状树):每个节点调用
- 空间复杂度:O(n)
- 递归调用栈深度(树的高度)
代码解释
class Solution {public int pathSum(TreeNode root, int targetSum) {if (root == null) return 0; // 空树无路径// 三部分路径总和:int ret = rootSum(root, targetSum); // 1. 以当前节点为起点的路径ret += pathSum(root.left, targetSum); // 2. 左子树中的路径ret += pathSum(root.right, targetSum);// 3. 右子树中的路径return ret;}public int rootSum(TreeNode root, long targetSum) {if (root == null) return 0; // 终止条件int ret = 0;// 检查当前节点是否满足目标值if (root.val == targetSum) ret++;// 递归处理子树(更新剩余目标值)ret += rootSum(root.left, targetSum - root.val);ret += rootSum(root.right, targetSum - root.val);return ret;}
}
解决的关键问题
- 路径起点不固定:通过外层递归遍历所有可能的起点
- 路径向下延伸:通过内层递归探索所有可能的路径终点
- 节点值可正可负:即使找到一条路径,仍需继续向下搜索(可能存在抵消路径)
该解法直观体现了分治思想,通过双重递归覆盖了所有可能的路径情况。对于更优解(O(n)时间复杂度),可使用前缀和+哈希表优化,但当前解法更易于理解二叉树路径问题的本质。
236. 二叉树的最近公共祖先
解题思路描述
这段代码解决了**二叉树的最近公共祖先(LCA)**问题:给定一棵二叉树和两个节点 p
和 q
,找到它们深度最大的公共祖先节点。核心思路是通过深度优先搜索(DFS)遍历整棵树,利用递归回溯时传递的信息判断节点间的祖先关系。
关键思路:递归标记 + 后序遍历
-
自底向上递归:
- 从叶子节点开始向上回溯,每个节点向父节点传递子树中是否包含目标节点
- 利用后序遍历(左→右→根)确保先处理子节点再处理父节点
-
节点状态标记:
- 定义递归函数
dfs(root, p, q)
表示当前子树是否包含p
或q
- 包含则返回
true
,否则返回false
- 定义递归函数
-
LCA 判定条件:
- 情况1:
p
和q
分别位于当前节点的左右子树中 →lson && rson == true
- 情况2:当前节点是
p
或q
,且子树包含另一个节点 →(root=p||root=q) && (lson||rson)
- 满足任一条件即记录当前节点为 LCA
- 情况1:
算法步骤详解
-
初始化:
private TreeNode ans; // 存储结果 public Solution() { this.ans = null; } // 初始化结果节点
-
深度优先搜索(DFS):
private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {if (root == null) return false; // 终止条件:空节点// 递归搜索左右子树boolean lson = dfs(root.left, p, q); // 左子树是否含 p/qboolean rson = dfs(root.right, p, q); // 右子树是否含 p/q// 判断当前节点是否为 LCAif ((lson && rson) || ((root == p || root == q) && (lson || rson))) {ans = root; // 记录最近公共祖先}// 返回当前子树是否包含 p 或 qreturn lson || rson || (root == p || root == q); }
-
启动搜索:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {dfs(root, p, q); // 启动 DFSreturn ans; // 返回结果 }
执行过程示例
假设二叉树如下,寻找节点 5
和 1
的 LCA:
3/ \5 1/ \ / \6 2 0 8/ \7 4
-
递归至节点
5
:- 左子树
6
:不包含目标 →false
- 右子树
2
:子树包含7
和4
,但非目标 →false
- 自身是
5
→ 返回true
- 判定条件不满足(子树无目标)→ 不记录 LCA
- 左子树
-
递归至节点
1
:- 子树
0
和8
非目标 →false
- 自身非目标 → 返回
false
- 不记录 LCA
- 子树
-
递归至节点
3
:- 左子树(节点
5
)返回true
- 右子树(节点
1
)返回false
- 自身非目标 → 返回
true
- 判定条件不满足 → 不记录 LCA
- 左子树(节点
-
找到 LCA:
- 实际 LCA 为节点
3
(满足lson && rson
条件) - 为何未记录?
节点5
的子树实际应返回true
(因包含目标5
),但节点2
的子树未正确传递状态
- 实际 LCA 为节点
修正关键:在节点
2
处:
- 左右子树(
7
和4
)不包含目标 → 返回false
- 但节点
5
的右子树2
应继承状态:因节点5
自身是目标,故子树应返回true
- 正确流程:
- 节点
5
的右子树2
返回false
- 节点
5
自身是目标 → 返回true
给父节点3
- 节点
1
返回false
- 节点
3
:lson=true
,rson=false
→ 不满足条件 - LCA 未被记录?
错误在树结构理解:p=5
,q=1
的 LCA 应为3
,此时节点3
满足lson && rson
?
重新分析:- 节点
3
的左子树包含5
(lson=true
) - 节点
3
的右子树包含1
(rson=true
) - 故满足
lson && rson
→ 记录ans=3
- 节点
- 节点
特性与复杂度
- 时间复杂度:O(n)
每个节点仅访问一次 - 空间复杂度:O(h)
递归栈深度 = 树的高度(最坏情况 O(n)) - 关键优势:
- 一次遍历解决问题
- 利用递归回溯天然的自底向上特性
- 精确的节点状态传递机制
关键逻辑说明
if ((lson && rson) || ((root == p || root == q) && (lson || rson))) {ans = root;
}
-
lson && rson
p
和q
分别位于左右子树 → 当前节点是分叉点 -
(root=p||root=q) && (lson||rson)
当前节点是p
或q
,且子树包含另一目标 → 当前节点即是 LCA
注意:使用
root == p
而非root.val == p.val
确保比较对象是节点本身而非值(避免值重复导致错误)
总结
该算法通过巧妙的递归状态传递:
- 标记子树包含目标节点的状态
- 利用后序遍历特性自底向上判断
- 在首次满足 LCA 条件时记录结果
- 完美处理了各种树结构情况(包括节点是自身祖先的情况)
这是解决 LCA 问题的经典深度优先搜索实现,兼顾了简洁性和效率。