题目
解析
方法一:中序遍历
二叉搜索树「中序遍历」得到的值构成的序列一定是升序的
中序遍历时,判断当前节点是否大于中序遍历的前一个节点,如果大于,说明满足 BST,继续遍历;否则直接返回 false。
class Solution {
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
// 访问左子树
if (!isValidBST(root.left)) {
return false;
}
// 访问当前节点:如果当前节点小于等于中序遍历的前一个节点,说明不满足BST,返回 false;否则继续遍历。
if (root.val <= pre) {
return false;
}
pre = root.val;
// 访问右子树
return isValidBST(root.right);
}
}
方法二:递归(上界和下界)
首先,我们来看二叉搜索树的两个特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
仔细思考这两句话,你可以理解为:
- 当前节点的值是其左子树的值的上界(最大值)
- 当前节点的值是其右子树的值的下界(最小值)
有了这个想法,你可以将验证二叉搜索树的过程抽象成下图(-00代表无穷小,+00代表无穷大):
有了这个图,你可以很清晰地将这个判断过程抽象成递归代码。
但是在给出代码之前,我们看一看写递归代码必需的两个要素:
- 终止条件
- 深入递归的递归方程
首先来看在这道题中的终止两种终止条件:
- 当当前节点为空时,表示这个节点已经是叶子节点,这个节点没有子节点,可以返回 True
- 当当前节点不在 [ min_value,max_value ] 的区间时,这个节点不能同时符合二叉搜索树的两个特征,返回 False
然后看看递归方程,由于节点有两个子树,所以我们有两个递归方程要执行:
- 对左子树:
helper(node.left, lower, val)
解释:当前节点是左子树的上界(你可以粗略地理解为最大值) - 对右子树:
helper(node.right, val, upper)
解释:当前节点值是右子树的下界,并且继承上一个节点的上界。
这个递归过程是最难理解的地方,如果不理解,你可以看一看上图中绿色剪头,你会很快理解这么递归的原因。
代码如下
class Solution {
public boolean helper(TreeNode node, Integer lower, Integer upper) {
if (node == null) return true;
int val = node.val;
if (lower != null && val <= lower) return false;
if (upper != null && val >= upper) return false;
if (! helper(node.right, val, upper)) return false;
if (! helper(node.left, lower, val)) return false;
return true;
}
public boolean isValidBST(TreeNode root) {
return helper(root, null, null);
}
}
复杂度分析
时间复杂度 : O(n),其中 n为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
空间复杂度 : O(n),其中 n 为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 n ,递归最深达到 n 层,故最坏情况下空间复杂度为 O(n) 。
递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 n ,递归最深达到 n 层,故最坏情况下空间复杂度为 O(n) 。