验证二叉搜索树

给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。

有效二叉搜索树定义如下:

  • 节点的左子树只包含严格小于当前节点的数。
  • 节点的右子树只包含严格大于当前节点的数。
  • 所有左子树和右子树自身也必须是二叉搜索树。

示例 1:

输入:root = [2,1,3]
输出:true

示例 2:

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5,但是右子节点的值是 4。

提示:

  • 树中节点数目范围在 [1, 10^4]
  • -2^31 <= Node.val <= 2^31 - 1

解法一(中序遍历):

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool isValidBST(TreeNode* root) {
        long long prev = LLONG_MIN;
        return inorder(root, prev);
    }

private:
    bool inorder(TreeNode* node, long long& prev) {
        if (node == nullptr)
        {
            return true;
        }

        if (!inorder(node->left, prev))
        {
            return false;
        }

        if (node->val <= prev)
        {
            return false;
        }

        prev = node->val;

        return inorder(node->right, prev);
    }
};

解法二(递归上下界):

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return dfs(root, LLONG_MIN, LLONG_MAX);
    }

private:
    bool dfs(TreeNode* node, long long lower, long long upper) {
        if (node == nullptr)
        {
            return true;
        }

        if (node->val <= lower || node->val >= upper)
        {
            return false;
        }

        return dfs(node->left, lower, node->val) && dfs(node->right, node->val, upper);
    }
};

核心思想

判断二叉搜索树时,不能只比较一个节点和它的左右孩子。

例如:

    5
   / \
  1   6
     / \
    3   7

节点 6 本身满足 3 < 6 < 7,但节点 3 位于根节点 5 的右子树中,它必须严格大于 5

所以这棵树不是二叉搜索树。

这题最关键的观察是:

二叉搜索树的限制不是只来自父节点,而是来自所有祖先节点。

常见做法有两种:

  • 中序遍历:有效二叉搜索树的中序遍历结果必须严格递增。
  • 递归上下界:每个节点都必须落在祖先节点传下来的合法区间内。

解法一:中序遍历

二叉搜索树有一个重要性质:

中序遍历结果严格递增

中序遍历顺序是:

左子树 -> 当前节点 -> 右子树

对于二叉搜索树来说:

  • 左子树所有节点值都小于当前节点。
  • 右子树所有节点值都大于当前节点。

所以按照中序遍历访问时,节点值应该越来越大。

代码中使用 prev 保存上一个访问到的节点值。

如果当前值满足:

node->val <= prev

说明中序序列不是严格递增,直接返回 false

这里必须使用严格大于,而不是大于等于。

因为题目要求二叉搜索树中不能出现等于当前节点的值,重复值也是不合法的。

为什么 prev 要用 long long

题目中节点值范围是:

-2^31 <= Node.val <= 2^31 - 1

如果直接把 prev 初始化为 INT_MIN,当树中第一个访问到的节点值也等于 INT_MIN 时,会误判为不满足严格递增。

所以代码使用:

LLONG_MIN

它比任何合法的 Node.val 都小,可以安全作为初始值。

解法二:递归上下界

对于每个节点,它都有一个合法取值范围:

(lower, upper)

当前节点值必须满足:

lower < node->val < upper

一开始根节点没有祖先限制,所以范围是:

(LLONG_MIN, LLONG_MAX)

当递归进入左子树时,左子树所有节点都必须小于当前节点。

所以左子树的范围变成:

(lower, node->val)

当递归进入右子树时,右子树所有节点都必须大于当前节点。

所以右子树的范围变成:

(node->val, upper)

只要某个节点不在自己的合法区间内,就说明它违反了某个祖先节点传下来的限制。

这种写法更直接地对应了二叉搜索树的定义,也是推荐解法。

为什么不能只比较左右孩子

只比较:

left->val < root->val < right->val

是不够的。

因为二叉搜索树要求的是:

  • 左子树所有节点都小于根节点。
  • 右子树所有节点都大于根节点。

不是只有左右孩子满足关系就可以。

示例:

root = [5,1,4,null,null,3,6]

节点 4 作为根节点 5 的右孩子,已经小于 5,所以这棵树不合法。

即使在节点 4 自己的子树里,3 < 4 < 6 成立,也不能改变它违反祖先限制这一点。

边界情况

如果当前节点为空,说明这是一棵空子树。

空子树不会违反二叉搜索树性质,所以返回 true

如果节点值等于边界值,也是不合法的。

因为二叉搜索树要求严格小于或严格大于。

如果树中存在重复值,中序遍历会出现相邻两个值相等,递归上下界中也会触发边界不满足。

因此重复值会被正确判定为非法。

如果节点值为 INT_MININT_MAX,使用 long long 边界可以避免初始边界和真实节点值冲突。

正确性证明

我们证明:两个解法都能正确判断一棵树是否是有效二叉搜索树。

结论 1:中序遍历解法不会把非法二叉搜索树判断为合法

如果一棵树不是有效二叉搜索树,那么一定存在某个节点违反了二叉搜索树的顺序要求。

对于二叉搜索树,中序遍历结果必须严格递增。

如果某个左子树节点不小于祖先节点,或者某个右子树节点不大于祖先节点,那么中序遍历中就会出现后访问的值不大于前一个值的情况。

算法会检查:

node->val <= prev

一旦出现这种情况,就返回 false

所以中序遍历解法不会把非法二叉搜索树判断为合法。

结论 2:中序遍历解法不会把合法二叉搜索树判断为非法

如果一棵树是有效二叉搜索树,那么左子树所有节点都严格小于根节点,右子树所有节点都严格大于根节点,并且左右子树自身也是二叉搜索树。

因此按中序遍历访问时,左子树中的所有值先出现,并且都小于根节点;根节点之后,右子树中的所有值出现,并且都大于根节点。

左右子树内部也满足同样性质。

所以整个中序遍历序列严格递增。

算法只会在当前值不大于前一个值时返回 false

合法二叉搜索树不会出现这种情况。

所以中序遍历解法不会把合法二叉搜索树判断为非法。

结论 3:递归上下界解法不会漏掉祖先限制

递归函数 dfs(node, lower, upper) 表示:

判断以 node 为根的子树中,所有节点是否都严格落在 (lower, upper) 内

对于当前节点,算法先检查:

lower < node->val < upper

如果不满足,立即返回 false

进入左子树时,算法把上界更新为 node->val

进入右子树时,算法把下界更新为 node->val

这样,祖先节点对当前子树的限制会通过 lowerupper 一直向下传递。

所以递归上下界解法不会漏掉任何祖先限制。

结论 4:递归上下界解法的判断条件和二叉搜索树定义一致

如果一个节点位于某个祖先的左子树中,它的值必须严格小于这个祖先。

如果一个节点位于某个祖先的右子树中,它的值必须严格大于这个祖先。

lower 保存当前节点必须严格大于的最大下界。

upper 保存当前节点必须严格小于的最小上界。

只要每个节点都满足:

lower < node->val < upper

并且左右子树递归检查也都成立,就说明每个节点都满足所有祖先传下来的二叉搜索树限制。

因此整棵树是有效二叉搜索树。

得出结论

由结论 1 和结论 2 可知,中序遍历解法判断结果正确。

由结论 3 和结论 4 可知,递归上下界解法判断结果正确。

因此两个解法都能正确判断二叉树是否是有效二叉搜索树。

举例理解

以:

root = [5,1,4,null,null,3,6]

为例,这棵树可以理解为:

    5
   / \
  1   4
     / \
    3   6

用递归上下界看:

节点 合法范围 是否满足
5 (LLONG_MIN, LLONG_MAX) 满足
1 (LLONG_MIN, 5) 满足
4 (5, LLONG_MAX) 不满足

节点 4 在根节点 5 的右子树中,所以它必须严格大于 5

4 < 5,因此整棵树不是有效二叉搜索树。

用中序遍历看,这棵树的中序结果是:

[1, 5, 3, 4, 6]

这个序列不是严格递增,因为 3 < 5

所以同样返回 false

复杂度分析

设二叉树节点数为 n,树的高度为 h

解法一

每个节点最多被访问一次。

  • 时间复杂度:O(n)
  • 空间复杂度:O(h)

空间复杂度来自递归调用栈。

解法二

每个节点最多被访问一次,并且每次只进行常数次比较。

  • 时间复杂度:O(n)
  • 空间复杂度:O(h)

空间复杂度来自递归调用栈。