LeetCode Notes_#98_验证二叉搜索树
Contents
题目
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/
1 3
输出: true
示例 2:
输入:
5
/
1 4
/
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
思路分析
整体来说,有两种思路。
- 前序遍历+上下界: BST当中每个节点都有上下界,如果所有节点的值在上下界规定的范围内,这个树就是二叉搜索树。通过前序遍历的顺序,逐个验证每个节点是否符合BST的要求,就可以判别二叉树是否是BST。
- 上下界的递推规律(从父节点的上下界递推左右孩子的上下界)
- 对于左孩子节点,它的上界是父节点的值,下界还是父节点的下界
- 对于右孩子节点,它的下界是父节点的值,上界还是父节点的上界
- 上下界的递推规律(从父节点的上下界递推左右孩子的上下界)
- 中序遍历: BST的中序遍历是递增序列。利用这个特性,我们中序遍历输入的二叉树,每一次都比较当前节点的值和前一个节点的值,如果不满足递增,就不是BST,都满足递增,就是BST。
具体来说,两种思路都可以写成递归和非递归的形式,都是在二叉树遍历模板的基础上进行修改,加入上述逻辑即可。
递归
本题的递归函数比较特殊,跟普通的二叉树遍历递归有点区别。
递归函数返回值类型是boolean
,含义是:当前的子树是否是BST。
终止条件
遇到null
节点,说明越过了叶子节点。将null
也看作一棵子树,null
其实也是一个BST,所以返回true
,由此开始回溯。
递推过程
两种方法的递推过程不同。
- 前序遍历 + 上下界
- 判定当前节点的值是否在上下界范围内,不在则返回
false
- 开启左右子树递归,更新左右子树的上下界,判定左右子树是否是BST,不是则返回
false
- 判定当前节点的值是否在上下界范围内,不在则返回
- 中序遍历
- 开启左子树递归,判定左子树是否是
BST
,不是则返回false
- 判定当前节点的值与前一个节点的值是否满足递增,不是则返回
false
;更新pre - 开启右子树递归,判定右子树是否是
BST
,不是则返回false
- 开启左子树递归,判定左子树是否是
回溯返回值
- 任何不满足条件的状况,返回
false
到上一层递归函数,最终结果也一定是false
- 一直没有返回
false
,返回true
到上一层递归函数
非递归
非递归写法是递归写法+非递归遍历模板
的组合,只需要简单改动非递归遍历的模板即可,不再详细分析。
解答
解答1:前序遍历+上下界+递归
class Solution {
public boolean isValidBST(TreeNode root) {
return recur(root, null, null);
}
//因为需要添加上下界参数lower,upper,所以需要重新定义一个递归方法
//整体来看是前序遍历,需要用两个参数记录上下界
private boolean recur(TreeNode node, Integer lower, Integer upper){
//null表示传入的子树是空树,空树也是二叉搜索树,所以返回true
if(node == null) return true;
int val = node.val;
if(lower != null && val <= lower) return false;
if(upper != null && val >= upper) return false;
//node的右子树node.right的下界修改为val,上界保持不变
if(!recur(node.right,val,upper)) return false;
//node的左子树node.left的上界修改为val,下界保持不变
if(!recur(node.left,lower,val)) return false;
return true;
}
}
解答2:前序遍历+上下界+非递归
class Solution {
//栈数据结构不一定非要用Stack类实现,也可以用LinkedList类
//初始化三个栈
LinkedList<TreeNode> stack = new LinkedList();//保存树节点
LinkedList<Integer> uppers = new LinkedList();//保存树节点对应的上界
LinkedList<Integer> lowers = new LinkedList();//保存树节点对应的下界
public boolean isValidBST(TreeNode root) {
if(root == null) return true;
Integer lower = null, upper = null;
//根节点入栈,upper,lower初始化为null
update(root, upper, lower);
while(!stack.isEmpty()){
//弹出一个节点,并弹出相应的上下界的值,用于判断当前节点是否符合BST定义
TreeNode node = stack.poll();
lower = lowers.poll();
upper = uppers.poll();
if(lower != null && node.val <= lower) return false;
if(upper != null && node.val >= upper) return false;
//压入右子树根节点,以及对应的上下界的值,待判断
//右子树根节点的下界修改为node.val
if(node.right != null) update(node.right, node.val, upper);
//压入左子树根节点,以及对应的上下界的值,待判断
//左子树根节点的上界修改为node.val
if(node.left != null) update(node.left, lower, node.val);
}
return true;
}
//同时向三个栈压入数据:当前访问到的树节点,当前节点对应的上界和下界
private void update(TreeNode root, Integer lower, Integer upper){
stack.add(root);
lowers.add(lower);
uppers.add(upper);
}
}
解答3:中序遍历+递归
class Solution {
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
//越过叶子节点,访问到null,null也是BST,返回true,开始回溯
if(root == null) return true;
if(!isValidBST(root.left)) return false;
if(root.val <= pre) return false;
pre = root.val;
if(!isValidBST(root.right)) return false;
//回溯到根节点,还是没有返回false,就返回true
return true;
}
}
这里的递归函数返回值是boolean
,所以可以将其写到if
语句当中。
解答4:中序遍历+非递归
class Solution {
public boolean isValidBST(TreeNode root) {
Stack<TreeNode> stack = new Stack();
long pre = Long.MIN_VALUE;
//只有最左边的一排节点入栈,右侧的节点根本没有入栈,所以不能只根据!stack.isEmpty()判断是否终止循环
while(!stack.isEmpty() || root != null){
//root代表一棵子树,首先要遍历到这棵子树的左下角节点,作为遍历的第一个节点
while(root != null){
stack.push(root);
root = root.left;
}
//如果root不是null,执行上述循环,弹出左下角节点
//如果root是null,不执行上述循环,弹出上一层的最左侧节点,即回溯到上一层
root = stack.pop();
//对出栈节点的操作:与pre做比较,然后更新pre
if(root.val <= pre) return false;
pre = root.val;
//最后访问右节点
root = root.right;
}
return true;
}
}
参考非递归中序遍历的模板,其实仅仅是对于出栈后的操作进行了简单修改。
class Solution {
public List<Integer> inOrderTraversal(TreeNode root) {
if (root == null) {
return new LinkedList<>();
}
List<Integer> res = new LinkedList<>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode node = root;
while(node != null || !stack.isEmpty()) {
while (node != null) {
stack.addLast(node);
node = node.left;
}
//出栈
node = stack.removeLast();
//对出栈节点的操作:加入res列表
res.add(node.val);
node = node.right;
}
return res;
}
}
复杂度分析
4种解法复杂度是一样的。
时间复杂度:O(n)
,都要遍历所有节点,所以是O(n)
。
空间复杂度:O(n)
- 递归:递归调用产生调用栈
- 非递归:使用辅助栈保存节点数据
总结
树的题目大多都跟树的遍历相关,需要掌握二叉树遍历模板,对于每一道树的题,都可以用模板来套。题目做完后,有意识的跟模板进行比较,建立联系。将来遇到类似题,可以举一反三。