leetcode98. 验证二叉搜索树
题目
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
输入:root = [2,1,3]
输出:true
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
思路和代码:
首先这题的解决办法可以分为两种,递归和迭代。
怎么判断一棵树是不是二叉搜索树?二叉搜索树的中序遍历是有序的。所以中序遍历,依次处理节点发现其严格递增,则必然是二叉搜索树。
一.递归
1.递归序
简单回顾一下二叉树的前中后序遍历的递归实现。
对于这样的一棵二叉树,它的递归序是什么?
def f(root):
# 空节点就返回
if not root:
return root
# 1.以上两行代码是第一次来到自己
# 左边递归遍历
f(root.left)
# 2.第二次来到自己 左树遍历完了之后得回到自己 然后才能调用下一句
# 右边递归遍历
f(root.right)
# 3.第三次来到自己 右树遍历完了之后第三次回到自己 只有回到自己才知道下行没语句了 自己才能返回
以上面的二叉树为例,先得到1,2,4,发现4的左边点为空,则第二次回到4,4的右节点为空,第三次回到4,此时序列是1,2,4,4,4。
2的左子树遍历完了之后又回到2,遍历2的右节点5, 5的做节点为空,第二次到5, 5的右节点为空,第三次到5…以此类推,得到最终的递归序是——
1,2,4,4,4,2,5,5,5,2,1,3,6,6,6,3,7,7,7,3,1。
对于每一个节点都会有三次遍历。
2.二叉树的前中后序递归实现
而前(先)序遍历是第一次遍历到该节点的时候处理节点(打印或者加到结果集中);中序是第二次遍历到该节点的时候处理节点;后续是第三次遍历到该节点的时候处理节点。
递归实现前序遍历(leetcode144):
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
def preOrder(root):
if not root:
return []
res.append(root.val)
preOrder(root.left)
preOrder(root.right)
res = []
preOrder(root)
return res
递归实现中序遍历的代码(leetcode94):
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
def inOrder(root):
if not root:
return res
inOrder(root.left)
res.append(root.val)
inOrder(root.right)
res = []
inOrder(root)
return res
递归实现后序遍历(leetcode145):
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
def postOrder(root):
if not root:
return res
postOrder(root.left)
postOrder(root.right)
res.append(root.val)
res = []
postOrder(root)
return res
3.本题的递归代码实现
回到这题中,在递归实现中序遍历的基础上改代码。第二次遍历到节点的时候不是再把节点值加到结果列表中,而是判断是否升序,不是返回False,是就更新当前节点值。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
prevalue = float("-inf")
def isBST(root):
# 空节点返回True
nonlocal prevalue
if not root:
return True
# 递归左子树 左子树不是二叉搜索树 直接返回False
if not isBST(root.left):
return False
else:
# 当前遍历的值应该比prevalue大 是则更新prevalue 否则返回False
if root.val <= prevalue:
return False
else:
prevalue = root.val
# 递归右子树
return isBST(root.right)
return isBST(root)
还有一个容易想到的简单粗暴的方法就是得到中序后进行去重排序,看是否和之前的中序结果是否一样,一样则说明是二叉搜索树。
代码:
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
def inOrder(root):
if not root:
return []
inOrder(root.left)
res.append(root.val)
inOrder(root.right)
res = []
inOrder(root)
inorder = res
return res == sorted(res) and len(res) == len(set(res))
4.nonlocal
上面的代码里,在prevalue前申明了nonlocal,非局部变量。不申明的话就会报错:
UnboundLocalError: local variable ‘prevalue’ referenced before assignment
在“if root.val <= prevalue:”这句代码报错,说局部变量prevalue在被赋值之前就被引用了。nonlocal声明的变量不是局部变量,也不是全局变量,而是外部嵌套函数内的变量。
nonlocal关键字修饰变量后标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,nonlocal位置会发生错误。
最上层的函数使用nonlocal修饰变量必定会报错“SyntaxError: no binding for nonlocal ‘prevalue’ found”(没有找到本地绑定的prevalue,就是说上一级函数里没有这个变量),如下:
5.global
我发现用关键词global也能通过,但是需要在函数外面里面共申明两次,代码如下:
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
global prevalue
prevalue = float("-inf")
def isBST(root):
global prevalue
# nonlocal prevalue
# 空节点返回True
if not root:
return True
# 递归左子树 左子树不是二叉搜索树 直接返回False
if not isBST(root.left):
return False
else:
# 当前遍历的值应该比prevalue大 是则更新prevalue 否则返回False
if root.val <= prevalue:
return False
else:
prevalue = root.val
# 递归右子树
return isBST(root.right)
return isBST(root)
如果isBST外面的“global prevalue”注释掉,会报错“NameError: name ‘prevalue’ is not defined”,如果外面有,函数里面没有申明global prevalue,又会报错“UnboundLocalError: local variable ‘prevalue’ referenced before assignment”。
就对global有点好奇。
global关键字用来在函数或其他局部作用域中使用全局变量。
函数内部可以改变函数外部的变量。如:
a = 1
def test():
global a
a += 1
test()
print(a)
此段代码输出值是2。
问,为什么这题中prevalue要申明两遍才能通过?
我的理解是,在改变a值的时候a就是全局变量,但是在def isValidBST()函数下的变量是这个函数内的内部变量(局部变量),需要先申明它是全局变量,然后def isBST()函数是在这个函数里面又嵌套的一个函数,在这个函数里面改变prevalue的值需要在申明prevalue是全局变量。
感觉确实是这个样子,如下面的测试代码。在test1函数里面申明的变量a不是全局变量,是局部变量!如果不申明一下,在test2()里面就算申明了global a依然会报错。但如果a写在test1外面,即使在test1里面把global a注释掉不再报错。
但是,在这题里,写成下面的代码会报错,与之前的区别在于把prevalue定义在isValidBST()外面,而isValidBST()里面没有再嵌套一个函数。
如果在prevalue定义前也审明一下global不会报错,但是会有错误的案例通不过,如[0]。我在isValidBST()输出了prevalue的值,发现在系统测试不同样例的时候,在报错的样例中prevalue会保存为上一个样例的值而不是-inf。反正就,蛮奇怪的。
二.迭代
1.二叉树的前中后序迭代实现
迭代不像递归有统一的逻辑,前中后序的逻辑各不相同。
前序遍历(头左右):
先把根节点放入栈中,然后弹出一个节点,处理该节点(打印或加入到结果集中),把该节点的右左孩子节点压入栈(如果有),注意顺序是右左,周而复始。
代码:
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
if not root:
return res
stack = [root]
while stack:
node = stack.pop()
res.append(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return res
后序遍历(左右头):
后序和先序比较相似,所以先说后序遍历。
还是一样,先把根节点加入栈中,弹出一个节点,把这个节点放到另一个辅助栈中,然后再把弹出节点的左右孩子节点加入到栈中,注意是左右顺序。这样入栈的顺序是头左右,到辅助栈的入栈顺序就是头右左,周而复始,最后弹出辅助栈的结果就是后序遍历,左右头。
代码:
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
if not root:
return res
stack = [root]
while stack:
node = stack.pop()
res.append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return res[::-1]
中序遍历(左头右)
和前后序遍历的逻辑不一样,中序遍历是先把左右的左边界的节点入栈,然后弹出一个节点,进行处理(打印或者加入到结果集中),对弹出节点的右节点也这么干,周而复始。
代码:
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
if not root:
return res
stack = []
while stack or root:
if root:
stack.append(root)
root = root.left
else:
root = stack.pop()
res.append(root.val)
root = root.right
return res
注意循环条件不再是while stack而是while stack or root。
2.本题的迭代实现代码
先中序遍历在排序和比较去重前后长短的代码就不贴了,就是中序遍历的代码后面再加一句判断。
迭代实现本题递归实现本题的思路本质都是一样的,在本来将节点值加入结果集处进行一下比较即可。
代码:
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
prevalue = float("-inf")
stack = []
while stack or root:
if root:
stack.append(root)
root = root.left
else:
root = stack.pop()
# 在这块处理
if root.val <= prevalue:
return False
else:
prevalue = root.val
root = root.right
return True
综上,这题目直接中序遍历,在此基础上稍稍改动就完了。