【LeetCode Hot100----08-二叉树篇中(06-10),包含多种方法,详细思路与代码,让你一篇文章看懂所有!】
6.543. 二叉树的直径
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
1/ \2 3/ \ 4 5
其最长路径是 5→2→1→3或 4→2→1→3,边数为 3,因此直径为 3。
核心思路分析
二叉树直径问题的关键 insight 是:任意节点的直径等于其左子树的最大深度加上右子树的最大深度。因此,我们需要:
- 计算每个节点的左、右子树深度
- 跟踪所有节点中 “左深度 + 右深度” 的最大值
- 这个最大值就是二叉树的直径
解法:深度优先搜索(DFS)
思路概述
通过后序遍历计算每个节点的深度,同时在遍历过程中更新最大直径:
- 递归计算左子树深度
- 递归计算右子树深度
- 当前节点的直径为左深度 + 右深度
- 更新全局最大直径
- 返回当前节点的深度(1 + 左右深度的最大值)
具体步骤
- 初始化一个变量记录最大直径(可以使用数组或包装类实现引用传递)
- 定义递归函数计算节点深度:
- 若节点为
null
,返回 0 - 递归计算左子树深度
- 递归计算右子树深度
- 计算当前节点的直径(左深度 + 右深度)
- 更新最大直径
- 返回当前节点的深度(1+max (左深度,右深度))
- 若节点为
- 从根节点开始调用递归函数
- 返回最大直径
代码实现
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;}
}public class BinaryTreeDiameter {private int maxDiameter = 0;public int diameterOfBinaryTree(TreeNode root) {// 计算深度的同时更新最大直径calculateDepth(root);return maxDiameter;}private int calculateDepth(TreeNode node) {if (node == null) {return 0;}// 计算左子树深度int leftDepth = calculateDepth(node.left);// 计算右子树深度int rightDepth = calculateDepth(node.right);// 更新最大直径:当前节点的左深度+右深度maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth);// 返回当前节点的深度return 1 + Math.max(leftDepth, rightDepth);}
}
执行流程示例
以上面的示例树为例:
-
调用
diameterOfBinaryTree(root)
,root 为节点 1 -
调用calculateDepth(1):
-
调用calculateDepth(2)
(左子树):
- 调用calculateDepth(4):
- 4 的左右子树都为 null,返回 0
- 左深度 = 0,右深度 = 0,当前直径 = 0,maxDiameter=0
- 返回 1(1+0)
- 调用calculateDepth(5):
- 5 的左右子树都为 null,返回 0
- 左深度 = 0,右深度 = 0,当前直径 = 0,maxDiameter 保持 0
- 返回 1(1+0)
- 节点 2 的左深度 = 1,右深度 = 1,当前直径 = 2,maxDiameter=2
- 返回 2(1+1)
- 调用calculateDepth(4):
-
调用calculateDepth(3)
(右子树):
- 3 的左右子树都为 null,返回 0
- 左深度 = 0,右深度 = 0,当前直径 = 0,maxDiameter 保持 2
- 返回 1(1+0)
-
节点 1 的左深度 = 2,右深度 = 1,当前直径 = 3,maxDiameter=3
-
返回 3(1+2)
-
-
最终返回 maxDiameter=3
复杂度分析
- 时间复杂度:O (n),其中 n 是二叉树的节点数。每个节点只需访问一次。
- 空间复杂度:O (h),其中 h 是二叉树的高度。递归调用栈的深度取决于树的高度,在最坏情况下(二叉树为链状),h=n,空间复杂度为 O (n)。
关键细节说明
- 直径定义:直径是路径的边数,而不是节点数。例如,两个相邻节点之间的直径是 1(1 条边)。
- 路径特性:最长路径可能不经过根节点,因此必须检查每个节点的左右子树深度之和。
- 递归设计:
- 递归函数返回的是当前节点的深度,用于计算父节点的直径
- 最大直径通过一个成员变量在递归过程中不断更新
- 空树处理:如果树为空(root=null),则直径为 0。
这种解法巧妙地将直径计算与深度计算结合在一起,通过一次遍历完成所有计算,效率很高,是解决二叉树直径问题的最优方法。
7.102. 二叉树的层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
二叉树的层序遍历(也称为广度优先遍历)是按照树的层次顺序(从根节点开始,逐层从左到右)访问所有节点的过程。例如,对于二叉树:
3/ \9 20/ \15 7
其层序遍历结果为 [[3], [9, 20], [15, 7]]
。
一、解法一:迭代法(队列实现)
(一)思路概述
利用队列先进先出(FIFO)的特性,逐层处理节点:
- 根节点入队
- 记录当前层的节点数量,依次出队并收集节点值
- 将当前层节点的左右子节点入队(下一层节点)
- 重复步骤 2-3,直到队列为空
核心是通过记录每一层的节点数量,实现 “一层处理完再处理下一层” 的逻辑。
(二)具体步骤
-
边界处理:若根节点为
null
,返回空列表。 -
初始化:创建队列并将根节点入队,创建结果列表
result
。 -
循环处理
:当队列不为空时:
- 记录当前层节点数量
size = queue.size()
。 - 创建当前层的列表
level
。 - 遍历当前层的size个节点:
- 出队节点,将其值加入
level
。 - 若节点有左子树,左子节点入队。
- 若节点有右子树,右子节点入队。
- 出队节点,将其值加入
- 将
level
加入result
。
- 记录当前层节点数量
-
返回结果:
result
。
(三)代码实现
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;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) {List<List<Integer>> result = new ArrayList<>();if (root == null) {return result; // 空树返回空列表}Queue<TreeNode> queue = new LinkedList<>();queue.offer(root); // 根节点入队while (!queue.isEmpty()) {int size = queue.size(); // 当前层节点数量List<Integer> level = new ArrayList<>();for (int i = 0; i < size; 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);}}result.add(level); // 当前层加入结果}return result;}
}
(四)执行流程示例(以上述示例树为例)
- 初始队列:
[3]
,result = []
。 - 第 1 次循环:size = 1
- 出队 3,
level = [3]
。 - 入队 3 的左右子节点 9、20,队列变为
[9, 20]
。 result
加入[3]
→[[3]]
。
- 出队 3,
- 第 2 次循环:size = 2
- 出队 9,
level
加入 9→[9]
;9 无子女,队列变为[20]
。 - 出队 20,
level
加入 20→[9, 20]
;入队 20 的子女 15、7,队列变为[15, 7]
。 result
加入[9, 20]
→[[3], [9, 20]]
。
- 出队 9,
- 第 3 次循环:size = 2
- 出队 15,
level
加入 15→[15]
;15 无子女,队列变为[7]
。 - 出队 7,
level
加入 7→[15, 7]
;7 无子女,队列空。 result
加入[15, 7]
→[[3], [9, 20], [15, 7]]
。
- 出队 15,
- 队列空,循环结束,返回结果。
二、解法二:递归法(DFS 实现)
(一)思路概述
利用递归的深度优先搜索,通过记录当前节点的层数,将节点值放入对应层的列表中:
- 递归遍历节点,同时传递当前层数。
- 若当前层的列表未创建,则新建并加入结果。
- 将节点值加入当前层的列表。
- 递归遍历左子树(层数 + 1)和右子树(层数 + 1)。
核心是通过层数参数,在 DFS 过程中 “记住” 节点所属的层,间接实现层序遍历。
(二)具体步骤
-
边界处理:若根节点为
null
,返回空列表。 -
初始化:创建结果列表
result
。 -
递归遍历:调用辅助函数dfs(root, 0, result),其中0
表示初始层数(根节点为第 0 层)。
- 辅助函数逻辑:
- 若当前层数等于
result
大小,说明该层列表未创建,新建列表并加入result
。 - 将当前节点值加入当前层的列表。
- 若左子树不为空,递归左子树(层数 + 1)。
- 若右子树不为空,递归右子树(层数 + 1)。
- 若当前层数等于
- 辅助函数逻辑:
-
返回结果:
result
。
(三)代码实现
import java.util.ArrayList;
import java.util.List;class TreeNode {// 节点定义同前
}class Solution {public List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> result = new ArrayList<>();if (root == null) {return result;}dfs(root, 0, result);return result;}// 辅助递归函数:node为当前节点,level为当前层数private void dfs(TreeNode node, int level, List<List<Integer>> result) {// 若当前层的列表未创建,新建并加入结果if (level == result.size()) {result.add(new ArrayList<>());}// 当前节点加入对应层的列表result.get(level).add(node.val);// 递归遍历左子树(层数+1)if (node.left != null) {dfs(node.left, level + 1, result);}// 递归遍历右子树(层数+1)if (node.right != null) {dfs(node.right, level + 1, result);}}
}
(四)执行流程示例(以上述示例树为例)
-
初始调用
dfs(3, 0, result)
,result = []
。 -
level=0等于result.size()
(0),新建列表加入result→[[]]。
- 3 加入
result.get(0)
→[[3]]
。 - 递归左子树
dfs(9, 1, result)
。
- 3 加入
-
level=1等于result.size()(1),新建列表加入result→[[3], []]。
- 9 加入
result.get(1)
→[[3], [9]]
。 - 9 无左子树,递归右子树(null,不执行)。
- 9 加入
-
返回上层,递归 3 的右子树dfs(20, 1, result)。
level=1
<result.size()
(2),直接操作result.get(1)
。- 20 加入
result.get(1)
→[[3], [9, 20]]
。 - 递归左子树
dfs(15, 2, result)
。
-
level=2等于result.size()(2),新建列表加入result→[[3], [9, 20], []]。
- 15 加入
result.get(2)
→[[3], [9, 20], [15]]
。 - 15 无子女,返回。
- 15 加入
-
递归 20 的右子树dfs(7, 2, result)。
- 7 加入
result.get(2)
→[[3], [9, 20], [15, 7]]
。 - 7 无子女,返回。
- 7 加入
-
所有递归结束,返回结果。
三、两种解法对比
解法 | 核心思想 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|---|
迭代法(队列) | 利用队列逐层处理节点 | O(n) | O (w)(w 为最大宽度) | 直观体现层序遍历过程 | 需要维护队列,代码稍长 |
递归法(DFS) | 记录层数,DFS 中分层收集 | O(n) | O (h)(h 为树高) | 代码简洁,无需手动维护队列 | 非直观,依赖层数参数 |
- 时间复杂度:两种方法均需访问所有节点一次,故为 O (n),n 为节点总数。
- 空间复杂度:
- 迭代法:队列最多存储一层的节点,最坏情况(满二叉树最后一层)为 O (n/2)≈O (n)。
- 递归法:递归栈深度为树高 h,最坏情况(链状树)为 O (n)。
- 适用性:
- 迭代法适合需要按层处理的场景(如分层打印、计算每层平均值)。
- 递归法适合代码简洁性优先的场景,或已熟悉 DFS 的实现思路。
总结
层序遍历的核心是 “按层次划分节点”,两种方法从不同角度实现这一目标:
- 迭代法通过队列显式分隔每层节点,逻辑直接,符合 “广度优先” 的直觉。
- 递归法通过深度优先遍历 + 层数标记,间接实现分层,代码更简洁。
实际应用中,迭代法是层序遍历的标准实现,因其更直观地反映了层序遍历的过程,且便于扩展到类似问题(如锯齿形层序遍历)。递归法则适合作为辅助思路,帮助理解不同遍历方式的联系。
8.108. 将有序数组转换为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
将有序数组转换为平衡二叉搜索树(BST)是一个经典问题。平衡 BST 的定义是:左右两个子树的高度差的绝对值不超过 1,且左右两个子树都是平衡二叉树。由于输入数组是升序的,这正好符合 BST 的中序遍历特性(左→根→右为升序),为构建提供了便利。
一、核心思路分析
平衡 BST 的构建关键在于选择合适的根节点,确保左右子树的节点数量尽可能均衡。对于升序数组:
- 数组的中间元素作为根节点,可使左右子树的节点数量差不超过 1,天然满足平衡性。
- 左半部分数组构建左子树,右半部分数组构建右子树(递归思想)。
二、解法一:递归法(选择中间元素为根)
(一)思路概述
利用分治思想,每次选择数组的中间元素作为根节点,递归构建左右子树:
- 找到当前数组的中间索引
mid
,以nums[mid]
为根节点。 - 左子树由
nums[0..mid-1]
递归构建。 - 右子树由
nums[mid+1..end]
递归构建。 - 递归终止条件:数组为空(返回
null
)。
(二)具体步骤
- 边界处理:若数组为空,返回
null
。 - 定义递归函数:buildBST(nums, left, right),表示用nums[left…right]构建 BST。
- 若
left > right
,返回null
(子数组为空)。 - 计算中间索引
mid = left + (right - left) / 2
(避免left + right
溢出)。 - 创建根节点
root
,值为nums[mid]
。 - 递归构建左子树:
root.left = buildBST(nums, left, mid-1)
。 - 递归构建右子树:
root.right = buildBST(nums, mid+1, right)
。 - 返回
root
。
- 若
- 初始调用:
buildBST(nums, 0, nums.length-1)
。
(三)代码实现
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 sortedArrayToBST(int[] nums) {if (nums == null || nums.length == 0) {return null;}return buildBST(nums, 0, nums.length - 1);}private TreeNode buildBST(int[] nums, int left, int right) {if (left > right) {return null; // 子数组为空,返回空节点}// 计算中间索引(避免溢出)int mid = left + (right - left) / 2;// 中间元素作为根节点TreeNode root = new TreeNode(nums[mid]);// 递归构建左子树(左半部分数组)root.left = buildBST(nums, left, mid - 1);// 递归构建右子树(右半部分数组)root.right = buildBST(nums, mid + 1, right);return root;}
}
(四)执行流程示例(以nums = [-10, -3, 0, 5, 9]
为例)
-
-
初始调用buildBST(nums, 0, 4):
-
mid = 0 + (4-0)/2 = 2
,根节点为0
(nums[2]
)。 -
左子树:buildBST(nums, 0, 1)。
mid = 0 + (1-0)/2 = 0
,根节点为-10
。- 左子树:
buildBST(nums, 0, -1)
→null
。 - 右子树:
buildBST(nums, 1, 1)
→根节点-3
(左右子树均为null
)。 - 左子树结果:
-10
的右子树为-3
,即-10→-3
。
-
右子树:buildBST(nums, 3, 4)。
mid = 3 + (4-3)/2 = 3
,根节点为5
。- 左子树:
buildBST(nums, 3, 2)
→null
。 - 右子树:
buildBST(nums, 4, 4)
→根节点9
(左右子树均为null
)。 - 右子树结果:
5
的右子树为9
,即5→9
。
-
最终树结构:
0/ \-10 5\ \-3 9
-
-
三、解法二:递归法(选择中间偏左 / 偏右元素为根)
(一)思路概述
平衡 BST 的构建不唯一,当数组长度为偶数时,可选择中间偏左或偏右的元素作为根节点,仍能保证平衡性:
- 中间偏左:
mid = left + (right - left) / 2
(与解法一相同)。 - 中间偏右:
mid = left + (right - left + 1) / 2
(适用于偶数长度数组)。
两种选择均能满足平衡条件,仅树的结构略有差异。
(二)代码实现(中间偏右示例)
class Solution {public TreeNode sortedArrayToBST(int[] nums) {if (nums == null || nums.length == 0) {return null;}return buildBST(nums, 0, nums.length - 1);}private TreeNode buildBST(int[] nums, int left, int right) {if (left > right) {return null;}// 中间偏右的索引(偶数长度时选择右侧中间元素)int mid = left + (right - left + 1) / 2;TreeNode root = new TreeNode(nums[mid]);root.left = buildBST(nums, left, mid - 1);root.right = buildBST(nums, mid + 1, right);return root;}
}
(三)执行流程示例(同数组[-10, -3, 0, 5, 9]
)
-
初始调用
mid = 0 + (4-0+1)/2 = 2
(与解法一相同,因长度为 5 是奇数)。 -
若数组为[-10, -3, 0, 5]
(长度 4,偶数):
-
中间偏右
mid = 0 + (3-0+1)/2 = 2
,根节点为0
。 -
左子树:
[-10, -3]
→根节点-3
(mid = 0 + (1-0+1)/2 = 1
)。 -
右子树:
[5]
→根节点5
。 -
树结构:
0/ \-3 5/ -10
-
四、解法三:迭代法(模拟递归过程)
(一)思路概述
用栈模拟递归的分治过程,栈中存储 “子数组范围 + 父节点 + 左右标记”,手动控制构建步骤:
- 栈初始化:压入整个数组范围
(left=0, right=n-1)
,父节点为null
,标记为 “根节点”。 - 循环处理栈顶元素:
- 弹出范围
(left, right)
,计算mid
,创建当前节点。 - 根据标记将当前节点连接到父节点的左 / 右子树。
- 压入右子数组范围
(mid+1, right)
,父节点为当前节点,标记为 “右子树”。 - 压入左子数组范围
(left, mid-1)
,父节点为当前节点,标记为 “左子树”(栈先进后出,确保左子树先处理)。
- 弹出范围
- 栈为空时,构建完成。
(二)代码实现
import java.util.Stack;class Solution {// 辅助类:存储子数组范围、父节点、是否为右子树static class StackNode {int left;int right;TreeNode parent;boolean isRight;StackNode(int left, int right, TreeNode parent, boolean isRight) {this.left = left;this.right = right;this.parent = parent;this.isRight = isRight;}}public TreeNode sortedArrayToBST(int[] nums) {if (nums == null || nums.length == 0) {return null;}Stack<StackNode> stack = new Stack<>();TreeNode root = null;// 初始压入整个数组范围,父节点为null,标记为非右子树(根节点)stack.push(new StackNode(0, nums.length - 1, null, false));while (!stack.isEmpty()) {StackNode node = stack.pop();int left = node.left;int right = node.right;TreeNode parent = node.parent;boolean isRight = node.isRight;if (left > right) {continue;}// 计算中间索引int mid = left + (right - left) / 2;TreeNode current = new TreeNode(nums[mid]);// 连接到父节点if (parent == null) {root = current; // 根节点} else {if (isRight) {parent.right = current;} else {parent.left = current;}}// 压入右子树范围(先压入,后处理)stack.push(new StackNode(mid + 1, right, current, true));// 压入左子树范围(后压入,先处理)stack.push(new StackNode(left, mid - 1, current, false));}return root;}
}
(三)执行流程示例(核心步骤)
以nums = [-10, -3, 0, 5, 9]
为例:
- 栈初始:
[(0,4,null,false)]
。 - 弹出(0,4,null,false),mid=2,创建节点0,根节点root=0。
- 压入右子树
(3,4,0,true)
和左子树(0,1,0,false)
,栈:[(3,4,0,true), (0,1,0,false)]
。
- 压入右子树
- 弹出(0,1,0,false),mid=0,创建节点-10,连接到0.left。
- 压入右子树
(1,1,-10,true)
和左子树(0,-1,-10,false)
,栈:[(3,4,0,true), (1,1,-10,true), (0,-1,-10,false)]
。
- 压入右子树
- 弹出
(0,-1,-10,false)
(left>right
,跳过)。 - 弹出(1,1,-10,true),mid=1,创建节点-3,连接到-10.right。
- 压入右子树
(2,1,-3,true)
和左子树(1,0,-3,false)
(均跳过),栈:[(3,4,0,true)]
。
- 压入右子树
- 后续处理右子树
(3,4,0,true)
,最终构建出与解法一相同的树。
五、三种解法对比
解法 | 核心思想 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|---|
递归法(中间根) | 分治 + 递归构建 | O(n) | O (log n)(递归栈) | 代码简洁,直观易懂 | 递归深度大时可能栈溢出 |
递归法(偏左 / 右根) | 分治 + 灵活选择根节点 | O(n) | O(log n) | 构建多种平衡 BST | 与标准解法类似,差异较小 |
迭代法(栈模拟) | 栈模拟递归分治过程 | O(n) | O (log n)(栈空间) | 无栈溢出风险 | 代码复杂,需辅助类 |
- 时间复杂度:三种方法均需访问数组所有元素一次,故为 O (n)。
- 空间复杂度:递归法的递归栈深度和迭代法的栈空间均为 O (log n)(平衡树的高度)。
- 唯一性:平衡 BST 的构建不唯一,选择不同的中间节点(如中间偏左 / 右)会得到不同但均平衡的树。
总结
将有序数组转换为平衡 BST 的核心是利用中间元素作为根节点,通过分治思想递归构建左右子树。递归法是最简洁的实现,迭代法通过栈模拟递归过程,适合对递归深度敏感的场景。由于平衡 BST 不唯一,可根据需求选择中间偏左或偏右的元素作为根节点,均能满足平衡性要求。
9.98. 验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 严格小于 当前节点的数。
- 节点的右子树只包含 严格大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树
注意:仅检查节点与其直接左右子节点的关系是不够的(如右子树的左节点可能小于根节点),必须保证整个子树的约束。
一、解法一:递归法(带上下界约束)
(一)思路概述
通过递归为每个节点设置合法值范围(下界和上界),确保节点值严格在范围内:
- 根节点的合法范围:
(-∞, +∞)
- 左子节点的合法范围:
(父节点下界, 父节点值)
(必须小于父节点) - 右子节点的合法范围:
(父节点值, 父节点上界)
(必须大于父节点)
若所有节点都在各自的合法范围内,则为有效的 BST。
(二)具体步骤
- 递归函数定义:
isValid(node, min, max)
,表示检查node
为根的子树是否在(min, max)
范围内。 - 终止条件:
- 若
node == null
:空树是 BST,返回true
。 - 若
node.val <= min
或node.val >= max
:超出范围,返回false
。
- 若
- 递归检查:
- 左子树:
isValid(node.left, min, node.val)
(左子树上限为当前节点值)。 - 右子树:
isValid(node.right, node.val, max)
(右子树下限为当前节点值)。 - 只有左右子树都有效时,返回
true
。
- 左子树:
- 初始调用:
isValid(root, Long.MIN_VALUE, Long.MAX_VALUE)
(用long
避免整数边界溢出)。
(三)代码实现
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 isValidBST(TreeNode root) {// 初始范围:(-∞, +∞),用Long避免int边界问题return isValid(root, Long.MIN_VALUE, Long.MAX_VALUE);}private boolean isValid(TreeNode node, long min, long max) {if (node == null) {return true; // 空树是有效的BST}// 节点值超出范围,无效if (node.val <= min || node.val >= max) {return false;}// 左子树范围:(min, node.val),右子树范围:(node.val, max)return isValid(node.left, min, node.val) && isValid(node.right, node.val, max);}
}
(四)执行流程示例
示例 1:有效 BST
2/ \1 3
- 调用isValid(2, -∞, +∞):2 在范围内。
- 左子树
isValid(1, -∞, 2)
:1 在(-∞, 2)
内,且 1 无子女→返回true
。 - 右子树
isValid(3, 2, +∞)
:3 在(2, +∞)
内,且 3 无子女→返回true
。
- 左子树
- 最终返回
true
。
示例 2:无效 BST(右子树左节点小于根)
5/ \1 4/ \3 6
- 调用isValid(5, -∞, +∞):5 在范围内。
- 左子树
isValid(1, -∞, 5)
→返回true
。 - 右子树
isValid(4, 5, +∞)
:4 <= 5(下界)→返回false
。
- 左子树
- 最终返回
false
。
二、解法二:中序遍历法(递归)
(一)思路概述
利用 BST 的核心特性:中序遍历结果为严格递增序列。通过递归中序遍历,记录前一个节点的值,若当前节点值 <= 前一个节点值,则不是有效的 BST。
(二)具体步骤
- 初始化:定义成员变量
prev
记录前一个节点值,初始为Long.MIN_VALUE
。 - 中序遍历递归函数:
- 若
node == null
,返回true
。 - 递归遍历左子树,若左子树无效,返回
false
。 - 检查当前节点:若
node.val <= prev
,返回false
;否则更新prev = node.val
。 - 递归遍历右子树,返回右子树的检查结果。
- 若
- 初始调用:从根节点开始中序遍历。
(三)代码实现
class TreeNode {// 节点定义同前
}class Solution {private long prev = Long.MIN_VALUE; // 记录前一个节点值public boolean isValidBST(TreeNode root) {return inorder(root);}private boolean inorder(TreeNode node) {if (node == null) {return true;}// 先检查左子树if (!inorder(node.left)) {return false;}// 检查当前节点是否大于前一个节点if (node.val <= prev) {return false;}prev = node.val; // 更新前一个节点值// 检查右子树return inorder(node.right);}
}
(四)执行流程示例(以示例 1 的有效 BST 为例)
-
prev = -∞,调用inorder(2)。
- 递归inorder(1):
- 递归
inorder(null)
→返回true
。 - 检查 1:1 > -∞→更新
prev=1
。 - 递归
inorder(null)
→返回true
。 - 左子树返回
true
。
- 递归
- 检查 2:2 > 1→更新
prev=2
。 - 递归inorder(3):
- 递归
inorder(null)
→返回true
。 - 检查 3:3 > 2→更新
prev=3
。 - 递归
inorder(null)
→返回true
。 - 右子树返回
true
。
- 递归
- 递归inorder(1):
-
最终返回
true
。
三、解法三:中序遍历法(迭代)
(一)思路概述
用栈模拟中序遍历过程,手动记录前一个节点值,实时检查序列的严格递增性。
(二)具体步骤
- 初始化:创建栈,
prev = Long.MIN_VALUE
,current = root
。 - 迭代中序遍历:
- 当current != null或栈不为空时:
- 将
current
的所有左节点入栈(深入左子树)。 - 弹出栈顶节点
node
,作为当前访问节点。 - 检查:若
node.val <= prev
,返回false
;否则更新prev = node.val
。 - 移动
current
到node.right
(处理右子树)。
- 将
- 当current != null或栈不为空时:
- 遍历完成未发现异常,返回
true
。
(三)代码实现
while(current!=null||!stack.isEmpty())条件拆解
current != null
:表示当前还有未访问的左子树节点需要入栈。
中序遍历的顺序是 “左→根→右”,因此需要先将所有左子节点依次入栈,直到最左节点(左子树为空)。!stack.isEmpty()
:表示栈中还有已入栈但未访问的节点(根节点或右子树的前驱节点)。
当左子树遍历完成后,需要从栈中弹出节点(访问根节点),然后处理其右子树。
import java.util.Deque;
import java.util.LinkedList;class TreeNode {// 节点定义同前
}class Solution {public boolean isValidBST(TreeNode root) {Deque<TreeNode> stack = new LinkedList<>();long prev = Long.MIN_VALUE;TreeNode current = root;while (current != null || !stack.isEmpty()) {// 左子树全部入栈while (current != null) {stack.push(current);current = current.left;}// 访问栈顶节点(当前最左节点)TreeNode node = stack.pop();if (node.val <= prev) {return false; // 不满足严格递增}prev = node.val; // 更新前一个节点值// 处理右子树current = node.right;}return true;}
}
(四)执行流程示例(以示例 2 的无效 BST 为例)
5/ \1 4/ \3 6
- 初始:
stack = []
,prev = -∞
,current = 5
。
- 左子树入栈:
current=5→1→null
,栈变为[5,1]
。 - 弹出 1:1 > -∞→
prev=1
,current=1.right=null
。 - 弹出 5:5 > 1→
prev=5
,current=5.right=4
。 - 4 的左子树入栈:
current=4→3→null
,栈变为[4,3]
。 - 弹出 3:3 > 5?3 <= 5→返回
false
(验证失败)。
四、三种解法对比
解法 | 核心思想 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|---|
递归法(上下界) | 为每个节点设置合法范围 | O(n) | O (h)(递归栈) | 直观体现 BST 约束,无冗余 | 递归深度大时可能栈溢出 |
中序递归法 | 利用中序遍历严格递增特性 | O(n) | O(h) | 代码简洁,依赖 BST 特性 | 需维护全局变量prev |
中序迭代法 | 栈模拟中序遍历,检查递增 | O(n) | O (h)(栈空间) | 无栈溢出风险,可控性强 | 代码稍复杂,需手动维护栈 |
- 时间复杂度:均为 O (n),需访问所有节点一次。
- 空间复杂度:均为 O (h),h 为树高(平衡树 h=log n,链状树 h=n)。
- 边界处理:均需考虑
int
极值(如[Integer.MAX_VALUE]
是有效 BST),故用long
存储边界或prev
。
总结
验证 BST 的核心是确保所有左子树节点 < 根节点 < 所有右子树节点:
- 递归法通过上下界约束直接体现这一规则,逻辑清晰。
- 中序遍历法利用 BST 的特性(中序递增),间接验证合法性,实现简单。
实际应用中,递归法(上下界)是最直接的选择,因其无需依赖 BST 的遍历特性,逻辑更通用;中序遍历法则更适合已熟悉 BST 特性的场景。两种方法均需注意整数边界的处理,避免误判。
10.230. 二叉搜索树中第 K 小的元素
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
小的元素(从 1 开始计数)。
二叉搜索树中第 K 小的元素:多种解法与实现思路
二叉搜索树(BST)的特性是:中序遍历结果为严格递增的有序序列。利用这一特性,我们可以高效地找到第 K 小的元素。
一、解法一:中序遍历(递归法)
(一)思路概述
根据 BST 中序遍历的有序性,第 K 次访问的节点即为第 K 小的元素:
- 递归进行中序遍历(左→根→右)。
- 记录访问节点的次数,当计数达到 K 时,当前节点即为结果。
(二)具体步骤
- 初始化:定义成员变量
count
记录访问次数,result
存储第 K 小的元素。 - 中序遍历递归函数:
- 若当前节点为
null
或已找到结果,直接返回。 - 递归遍历左子树。
- 访问当前节点:
count++
,若count == k
,记录result = node.val
并返回。 - 递归遍历右子树。
- 若当前节点为
- 初始调用:从根节点开始遍历,最终返回
result
。
(三)代码实现
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 {private int count = 0;private int result = 0;public int kthSmallest(TreeNode root, int k) {inorder(root, k);return result;}private void inorder(TreeNode node, int k) {if (node == null || count >= k) {return; // 终止条件:节点为空或已找到结果}// 遍历左子树inorder(node.left, k);// 访问当前节点count++;if (count == k) {result = node.val;return;}// 遍历右子树inorder(node.right, k);}
}
(四)执行流程示例(BST 如下,k=3)
plaintext
5/ \3 6/ \2 4/
1
中序遍历顺序为:1 → 2 → 3 → 4 → 5 → 6,第 3 小元素为 3。
-
初始
count=0
,调用inorder(5, 3)
。 -
递归左子树
inorder(3, 3)
:
-
递归左子树
inorder(2, 3)
:
-
递归左子树
inorder(1, 3)
:
- 1 的左子树为空,返回。
- 访问 1:
count=1
(≠3)。 - 1 的右子树为空,返回。
-
访问 2:
count=2
(≠3)。 -
2 的右子树为空,返回。
-
-
访问 3:
count=3
(=3),记录result=3
,返回。
-
-
后续遍历终止,最终返回 3。
二、解法二:中序遍历(迭代法,栈实现)
(一)思路概述
用栈模拟中序遍历过程,每弹出一个节点(访问)就计数,当计数达到 K 时,当前节点即为结果。相比递归法,迭代法可以在找到结果后立即终止,无需遍历完整棵树。
(二)具体步骤
- 初始化:创建栈,
current
指向根节点,count=0
。 - 迭代中序遍历:
- 当current != null或栈非空时:
- 将
current
的所有左节点入栈(深入左子树)。 - 弹出栈顶节点
node
,count++
。 - 若
count == k
,返回node.val
。 - 移动
current
到node.right
(处理右子树)。
- 将
- 当current != null或栈非空时:
(三)代码实现
class Solution {public int kthSmallest(TreeNode root, int k) {Deque<TreeNode> stack = new LinkedList<>();TreeNode current = root;int count = 0;while (current != null || !stack.isEmpty()) {// 左子树全部入栈while (current != null) {stack.push(current);current = current.left;}// 访问栈顶节点TreeNode node = stack.pop();count++;if (count == k) {return node.val; // 找到第k小元素,直接返回}// 处理右子树current = node.right;}return -1; // 理论上不会执行(k合法时)}
}
(四)执行流程示例(同前例,k=3)
- 初始
current=5
,stack=[5]
→current=3
→stack=[5,3]
→current=2
→stack=[5,3,2]
→current=1
→stack=[5,3,2,1]
→current=null
。 - 弹出 1:
count=1
(≠3),current=1.right=null
。 - 弹出 2:
count=2
(≠3),current=2.right=null
。 - 弹出 3:
count=3
(=3),返回 3。
三、解法三:优化解法(记录子树节点数)
(一)思路概述
对于频繁查询第 K 小元素的场景,可在每个节点中记录左子树的节点数量,实现 O (h) 时间复杂度的查询(h 为树高):
- 若当前节点左子树的节点数
leftCount >= k
:第 K 小元素在左子树中。 - 若
leftCount + 1 == k
:当前节点即为第 K 小元素。 - 若
leftCount + 1 < k
:第 K 小元素在右子树中,需查找右子树的第k - (leftCount + 1)
小元素。
(二)具体步骤
- 预处理:为每个节点计算左子树的节点数(可在插入 / 删除时维护)。
- 查询逻辑:根据当前节点左子树节点数与 K 的关系,递归缩小查找范围。
(三)代码实现
class TreeNode {int val;int leftCount; // 左子树节点数量TreeNode left;TreeNode right;TreeNode() {}TreeNode(int val) { this.val = val; }TreeNode(int val, int leftCount, TreeNode left, TreeNode right) {this.val = val;this.leftCount = leftCount;this.left = left;this.right = right;}
}class Solution {public int kthSmallest(TreeNode root, int k) {if (root == null) {return -1;}// 左子树节点数 >= k,第k小在左子树if (root.leftCount >= k) {return kthSmallest(root.left, k);}// 左子树节点数 + 1 == k,当前节点是第k小else if (root.leftCount + 1 == k) {return root.val;}// 否则在右子树,查找第 k - (leftCount + 1) 小else {return kthSmallest(root.right, k - (root.leftCount + 1));}}// 辅助方法:计算节点左子树的节点数(实际应用中需在插入时维护)private int calculateLeftCount(TreeNode node) {if (node == null || node.left == null) {return 0;}return countNodes(node.left);}// 计算子树总节点数private int countNodes(TreeNode node) {if (node == null) {return 0;}return 1 + countNodes(node.left) + countNodes(node.right);}
}
(四)执行流程示例(同前例,k=3)
树中各节点的 leftCount
如下:
- 1:0(无左子树)
- 2:1(左子树含 1)
- 3:2(左子树含 1、2)
- 4:0
- 5:3(左子树含 1、2、3、4 → 左子树节点数为 4?修正:3 的左子树节点数为 2,因此 5 的 leftCount 为 3(3 节点 + 其左子树 2 节点))
- 6:0
- 初始节点为 5,
leftCount=3
。 3 >= 3
→ 查找左子树(节点 3)。- 节点 3 的
leftCount=2
。 2 < 3
且2 + 1 = 3 == k
→ 当前节点 3 即为结果,返回 3。
四、三种解法对比
解法 | 核心思想 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|---|
递归中序遍历 | 利用中序遍历有序性,计数到 K | O(h + k) | O (h)(递归栈) | 一次性查询,实现简单 |
迭代中序遍历 | 栈模拟遍历,找到结果立即终止 | O(h + k) | O (h)(栈空间) | 一次性查询,效率略高 |
记录子树节点数 | 预处理左子树节点数,快速定位 | O (h)(查询) | O (n)(存储节点数) | 频繁查询,需预处理维护 |
- h 为树高:平衡 BST 中 h=log n,最坏情况(链状)h=n。
- k 的影响:前两种方法的时间与 k 相关(找到即终止),适合 k 较小时;优化解法与 k 无关,适合 k 较大时。
总结
BST 第 K 小元素的求解核心是利用其中序遍历有序性:
- 递归 / 迭代中序遍历是最直接的解法,实现简单,无需额外预处理。
- 记录子树节点数的方法适合频繁查询的场景,通过空间换时间提升效率。
实际应用中,若查询不频繁,迭代中序遍历是最优选择(无递归栈溢出风险,且可立即终止);若查询频繁,可采用优化解法并在树结构变更时维护节点计数。