文章目录
98. 验证二叉搜索树
给你一个二叉树的根节点 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
思路
要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。
有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。
递归法
可以递归中序遍历将二叉搜索树转变成一个切片,代码如下:
func dfs(root *TreeNode,arr *[]int) {
if root == nil {
return
}
dfs(root.Left,arr) // 左
*arr = append(*arr,root.Val) // 中
dfs(root.Right,arr) // 右
}
然后只要比较一下,这个切片是否是有序的,注意二叉搜索树中不能有重复元素。
// 数组严格递增
for i := 1;i < len(arr);i++ {
// 注意要小于等于,搜索树里不能有相同元素
if arr[i] <= arr[i - 1] {
return false
}
}
return true
整体Go
代码如下:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isValidBST(root *TreeNode) bool {
/*要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。
可以递归中序遍历将二叉搜索树转变成一个数组
然后只要比较一下,这个数组是否是有序的,注意二叉搜索树中不能有重复元素。
*/
if root == nil {
return true
}
arr := make([]int,0)
dfs(root,&arr)
// 数组严格递增
for i := 1;i < len(arr);i++ {
if arr[i] <= arr[i - 1] {
return false
}
}
return true
}
func dfs(root *TreeNode,arr *[]int) {
if root == nil {
return
}
dfs(root.Left,arr) // 左
*arr = append(*arr,root.Val) // 中
dfs(root.Right,arr) // 右
}
以上代码中,我们把二叉树转变为切片来判断,是最直观的,但其实不用转变成数组,可以在递归遍历的过程中直接判断是否有序。
这道题目比较容易陷入两个陷阱:
陷阱1
不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了。
写出了类似这样的代码:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isValidBST(root *TreeNode) bool {
if root == nil {
return true
}
if root.Left != nil && root.Val <= root.Left.Val {
return false
}
if root.Right != nil && root.Val >= root.Right.Val {
return false
}
return isValidBST(root.Left) && isValidBST(root.Right)
}
我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点。所以以上代码的判断逻辑是错误的。
例如: [10,5,15,null,null,6,20]
这个case
:
节点10
大于左节点5
,小于右节点15
,但右子树里出现了一个6
这就不符合了!
即上面每个节点都符合大于左节点
,小于右节点
,但是不符合每个节点大于左子树中所有节点
,小于右子树中所有节点
。
陷阱2
样例中最小节点 可能是int
的最小值,如果这样使用最小的int
来比较也是不行的。
了解这些陷阱之后我们来看一下代码应该怎么写:
递归三部曲:
确定递归函数,返回值以及参数
我们需要将左子树和右子树的取值范围限制住,所以需要两个参数min和max,而题目所给的数值可能就是int
值的边界,我们进行+1
和-1
操作会越界,所以定义为int64
类型
func dfs(root *TreeNode,min,max int64) bool {}
我们是在寻找一个不符合条件的节点,如果没有找到这个节点就遍历了整个树,如果找到不符合的节点了,则可以立刻返回。
代码如下:
if int64(root.Val) < min || int64(root.Val) > max {
return false
}
确定终止条件
如果是空节点 是不是二叉搜索树呢?
是的,二叉搜索树也可以为空!
代码如下:
if root == nil {
return true
}
确定单层递归的逻辑
限定左右子树的范围,递归左右子树。
//由于root.Val的值可能开始就是int的边界,故-1和+1可能会越界,因此转成int64型
left := dfs(root.Left,min,int64(root.Val) - 1) // 左子树取数范围
right := dfs(root.Right,int64(root.Val) + 1,max) // 右子树取数范围
return left && right
Go
整体代码如下:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isValidBST(root *TreeNode) bool {
/*乍一看,这是一个平凡的问题。只需要遍历整棵树,检查 node.right.val > node.val 和
node.left.val < node.val 对每个结点是否成立。
问题是,这种方法并不总是正确。不仅右子结点要大于该节点,整个右子树的元素都应该大于该节点
这意味着我们需要在遍历树的同时保留结点的上界与下界,判断该结点的值是否在范围内。*/
if root == nil {
return true
}
//最初根节点的值在int范围内即可
return dfs(root,math.MinInt64,math.MaxInt64)
}
func dfs(root *TreeNode,min,max int64) bool {
if root == nil {
return true
}
if int64(root.Val) < min || int64(root.Val) > max {
return false
}
//由于root.Val的值可以一开始就是int的边界,故-1和+1可能会越界,因此转成int64型
left := dfs(root.Left,min,int64(root.Val) - 1) // 左子树取数范围
right := dfs(root.Right,int64(root.Val) + 1,max) // 右子树取数范围
return left && right
}
如果题目给的就是int64
类型,则应该用左闭右闭,避免-1
和+1
操作,如下
func isValidBST(root *TreeNode) bool {
// 二叉搜索树也可以是空树
if root == nil {
return true
}
// 由题目中的数据限制可以得出min和max
return check(root,math.MinInt64,math.MaxInt64)
}
func check(node *TreeNode,min,max int64) bool {
if node == nil {
return true
}
if min >= int64(node.Val) || max <= int64(node.Val) {
return false
}
// 分别对左子树和右子树递归判断,如果左子树和右子树都符合则返回true
return check(node.Right,int64(node.Val),max) && check(node.Left,min,int64(node.Val))
}
迭代法
迭代法中序遍历稍加改动就可以了,Go
代码如下:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isValidBST(root *TreeNode) bool {
if root == nil {
return true
}
curTreeNode := root
var preTreeNode *TreeNode // 记录前一个节点
stack := make([]*TreeNode,0)
for curTreeNode != nil || len(stack) != 0 {
if curTreeNode != nil {
stack = append(stack,curTreeNode)
curTreeNode = curTreeNode.Left // 左
} else {
curTreeNode = stack[len(stack)-1] // 中
stack = stack[0:len(stack) - 1]
// 二叉搜索树的中序遍历会严格递增,不符合时可以直接返回false
if preTreeNode != nil && curTreeNode.Val <= preTreeNode.Val {
return false
}
preTreeNode = curTreeNode // 记录请一个访问的节点
curTreeNode = curTreeNode.Right // 右
}
}
return true
}
总结
这道题目是一个简单题,但对于没接触过的同学还是有难度的。
所以初学者刚开始学习算法的时候,看到简单题目没有思路很正常,千万别怀疑自己智商,学习过程都是这样的,大家智商都差不多。
只要把基本类型的题目都做过,总结过之后,思路自然就开阔了,加油💪