LeetCode算法日记 - Day 53: 验证二叉搜索树、二叉搜索树的第K小元素
本文介绍了二叉搜索树的两个经典问题及解法。第一部分讲解如何验证二叉树是否为二叉搜索树,通过维护节点值区间(low,high),递归检查每个节点是否满足严格区间约束,使用long类型避免边界值问题,时间复杂度O(n)。第二部分解决查找第k小元素问题,利用中序遍历特性,通过计数器remain在遍历过程中提前终止,时间复杂度O(h+k)。两问题均采用递归解法,并分析了可能出现的边界情况和优化空间。
目录
1. 验证二叉搜索树
https://leetcode.cn/problems/validate-binary-search-tree/description/
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 严格小于 当前节点的数。
- 节点的右子树只包含 严格大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3] 输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6] 输出:false 解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
- 树中节点数目范围在
[1, 104]
内 -231 <= Node.val <= 231 - 1
1.1 题目解析
题目本质
把“每个节点值必须大于左子树所有值、小于右子树所有值”转化为:对每个节点,维护它必须落在的开放区间 (low, high)。这个区间来自它所有祖先给它的“家规”。
常规解法
直觉会想:对每个节点递归求左子树最大值、右子树最小值,再比较 leftMax < node.val < rightMin。
问题分析
-
如果每次都去整棵左/右子树找极值,最坏会反复扫描,时间复杂度退化到 O(n²)。
-
还容易忽略“祖先”的约束(深层右-左节点必须同时大于根)。
-
用 int 边界容易在 Integer.MIN_VALUE / MAX_VALUE 附近踩坑(等于边界时被误判)。
思路转折
要想 O(n) 地一次遍历解决,就把“祖先的约束”随着递归一起下传:
-
访问一个节点前,它已经拿到了祖先们合并后的允许区间 (low, high);
-
检查 low < node.val < high,不满足直接剪枝返回 false;
-
继续把更新后的家规传给左右孩子:左子树 (low, node.val),右子树 (node.val, high)。 此外,用 long 作为边界 避免边界值误判。
1.2 解法
算法思想
• 维护每个节点的允许区间 (low, high);
• 空节点合法;
• 若 node.val <= low 或 node.val >= high,违规;
• 左递归用 (low, node.val),右递归用 (node.val, high);
• 初始用整域:(Long.MIN_VALUE, Long.MAX_VALUE)。
i)定义辅助函数 isValid(node, low, high)。
ii)若 node == null 返回 true。
iii)检查当前值是否落在 (low, high) 内(严格不等)。
iv)递归校验左子树:isValid(node.left, low, node.val)。
v)递归校验右子树:isValid(node.right, node.val, high)。
vi)两侧都为真则返回真,否则返回假。
易错点
-
必须是严格不等:< 和 >,不能允许相等。
-
只传一个边界会漏掉“祖先约束”,必须同时维护 low 和 high。
-
初始边界若用 int 的 MIN/MAX,当节点值等于边界会被误判;用 long 更安全。
-
不要把空子树当 0 或其它值参与比较——空子树直接返回 true。
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 isValidBST(TreeNode root) {
return isValid(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValid(TreeNode node, long low, long high) {
// 1) 空树合法
if (node == null) return true;
// 2) 当前节点必须在 (low, high) 的开放区间内(严格不等)
if (node.val <= low || node.val >= high) return false;
// 3) 递归校验左右子树,并分别更新家规
return isValid(node.left, low, node.val)
&& isValid(node.right, node.val, high);
}
}
复杂度分析
-
时间复杂度:每个节点至多访问一次,O(n)。
-
空间复杂度:递归栈深度为树高,O(h)(最坏退化链表时 O(n),平衡树时 O(log n))。
2. 二叉搜索树的第K小元素
https://leetcode.cn/problems/kth-smallest-element-in-a-bst/
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
小的元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1 输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3 输出:3
提示:
- 树中的节点数为
n
。 1 <= k <= n <= 104
0 <= Node.val <= 104
2.1 题目解析
题目本质
在二叉搜索树(BST)里,中序遍历(左→根→右)会按非降序输出所有值;因此“第 k 小”就是“中序序列里的第 k 个”。
常规解法
把整棵树做一次中序遍历,收集到数组,然后返回 arr[k-1]。
问题分析
完整收集需要 O(n) 时间与 O(n) 额外空间;而题目只要前 k 个元素,没必要遍历/存储剩余节点。极端情况下(k 很小),完整遍历做了大量“无用功”。
思路转折
要想更快 → 把中序遍历与计数结合,只要数到第 k
个就立刻停止。
这带来两层优化预期:
-
单次查询时,时间降到“走到第 k 个为止”,即 O(h + k)(h 为树高);
-
多次查询时,可在每个节点维护“左子树大小 sizeLeft”,自顶向下比较 k 与 sizeLeft,每次查询 O(h),适合频繁查询的场景。
2.2 解法
算法思想
• 中序遍历序列是升序。维护一个全局/外部变量 remain = k,每访问一个节点就 --remain;当 remain == 0 时,当前节点值就是答案,随后剪枝返回,不再继续遍历。
• 若需要“多次查询”,利用“秩选择”思想:设 L = size(node.left),若 k ≤ L 走左;若 k == L + 1 返回当前;否则令 k = k - (L + 1) 走右。
i)准备两个字段:remain = k,ans 存答案。
ii)递归函数 dfs(node):若 node == null 或 remain == 0 直接返回(剪枝)。
iii)递归左子树 dfs(node.left)。
iv)返回到当前 node 时,执行 --remain;若变为 0,记录 ans = node.val 并返回(后续全靠入口处的剪枝停止)。
v)递归右子树 dfs(node.right)。
vi)主函数调用 dfs(root),最后返回 ans。
易错点
-
忘记剪枝:找到第 k 个后,如不在函数入口处判断 remain == 0,右子树还会继续遍历,既耗时又可能把 remain 变成负数。
-
复用同一个 Solution 实例时,全局状态必须重置(remain、ans)。
-
极端退化链(高度 ≈ n)时,递归可能接近 JVM 栈深度上限;
2.3 代码实现
class Solution {
private int remain; // 还需要访问多少个节点
private int ans;
public int kthSmallest(TreeNode root, int k) {
this.remain = k;
this.ans = -1;
dfs(root);
return ans;
}
private void dfs(TreeNode node) {
if (node == null || remain == 0) return; // 基例 + 剪枝
dfs(node.left); // 先走左
if (remain == 0) return; // 左子树已经找到就不要再处理当前与右子树
if (--remain == 0) { // 访问“当前”节点
ans = node.val;
return; // 已经确定答案,交给入口剪枝阻断后续递归
}
dfs(node.right); // 再走右
}
}
复杂度分析
-
时间复杂度:O(n)(最坏)/ O(h + k)(更精细)。
-
空间复杂度:O(h),即递归深度。
更多推荐
所有评论(0)