Given the root
of a binary tree, determine if it is a valid binary search tree (BST).
A valid BST is defined as follows:
Example 1:
Input: root = [2,1,3] Output: true
Example 2:
Input: root = [5,1,4,null,null,3,6] Output: false Explanation: The root node's value is 5 but its right child's value is 4.
Constraints:
[1, 10^4]
.-2^31 <= Node.val <= 2^31 - 1
The description was taken from https://leetcode.com/problems/validate-binary-search-tree/.
#Recursive DFS
#O(N) Time, O(N) Space
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
if not root:
return False
def dfs(node: TreeNode, low:int, high:int) -> bool:
if not node:
return True
if node.val <= low or node.val >= high:
return False
valid_left = dfs(node.left, low, node.val)
valid_right = dfs(node.right, node.val, high)
return valid_left and valid_right
return dfs(root, -2**31-1, 2**31)
#Iterative DFS
#O(N) Time, O(N) Space
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
if not root:
return False
def dfs(root: TreeNode, low:int, high:int) -> bool:
stack = [(root, low, high)]
while stack:
node, low, high = stack.pop()
if node.val <= low or node.val >= high:
return False
if node.left:
stack.append((node.left, low, node.val))
if node.right:
stack.append((node.right, node.val, high))
return True
return dfs(root, -2**31-1, 2**31)
In a BST, each left subtree has to be less than the parent and each right subtree has to be greater. There also can't be duplicate values.
While keeping those rules in mind, we can solve this problem by performing a Depth-First Search.
The description also states that a node value can't be less than the lowest thirty-two-bit integer or greater than the greatest thirty-two-bit integer.
If we have traversed the tree with each of these requirements and constraints met, we have validated the tree.
The recursive solution for this problem is fairly short.
First off, if our root node value is null then it fails the given constraints so we will return false.
if not root:
return False
If that is not the case then let's start by building our dfs
function which will require a node, a lower bound, and an upper bound for each function call.
def dfs(node: TreeNode, low:int, high:int) -> bool:
Now let's set our base cases.
If we are at a null node, then that means we have exhausted the search of a subtree branch while every node we encountered followed the right requirements and constraints. If that is the case, we will return true to let our parent function calls know that everything was good from here.
if not node:
return True
Next, if our node value is less than or equal to our lower bound, that can mean a few things:
If our node value is greater than or equal to our upper bound, that will break similar rules:
If any of those cases are true, this node we just encountered invalidated the subtree so we will return false.
if node.val <= low or node.val >= high:
return False
After we have checked this node against those two conditional base cases, we will check both children as well.
If we have a left child, it can't be lower than the lower bound and it can't be greater than the current node.
valid_left = dfs(node.left, low, node.val)
If we have a right child, it can't be greater than the upper bound and it can't be less than the current node.
valid_right = dfs(node.right, node.val, high)
We'll check if both of those conditions are true.
return valid_left and valid_right
Now that our dfs
function is built, all we need to do is make the initial call by passing one number lower than our node value's possible lower bounds, and one number higher than our node value's possible upper bounds. We do this because we want these values to be overwritten later by values that are actually within the allowed bounds of a test case. We could also choose to use negative infinity and infinity, but this is a slight optimization.
return dfs(root, -2**31-1, 2**31)
If each function on the calls stack returns true, it means that the DFS traversed past the end of each branch and validated each subtree branch along the way.
The iterative version follows the same exact formula.
The only difference is we will be using a loop and a stack in place of a recursive call stack.
First off, if our root node value is null then it fails the given constraints so we will return false.
if not root:
return False
If it is not, then let's start by building our dfs
function which will require a node, a lower bound, and an upper bound for the initial call.
def dfs(root: TreeNode, low:int, high:int) -> bool:
Then, we will push those starting values given as the function argument into a stack.
stack = [(root, low, high)]
Next, we will create a loop that will continue to run while the stack still has child nodes to process.
while stack:
At the beginning of each iteration, we will pop a node, a lower bound, and an upper bound off of the stack.
node, low, high = stack.pop()
Just to reiterate what was mentioned in the recursive version, if our node value is less than or equal to our lower bound, that can mean a few things:
If our node value is greater than or equal to our upper bound, that will break similar rules:
If any of those cases are true, this node we just encountered invalidated the subtree so we will return false.
if node.val <= low or node.val >= high:
return False
After we have checked the current node against that condition, we will check both children as well, if they exist. If they do, we will push them onto the stack.
If we have a left child, it can't be lower than the lower bound and it can't be greater than the current node.
if node.left:
stack.append((node.left, low, node.val))
If we have a right child, it can't be greater than the upper bound and it can't be less than the current node.
if node.right:
stack.append((node.right, node.val, high))
If we have emptied the stack and broke from the loop then that means the DFS traversed to the end of each branch and validated each subtree branch along the way.
If that is the case, we will return true.
return True
Same as in the recursive solution, now that our dfs
function is built, all we need to do is make the initial call by passing one number lower than our node value's possible lower bounds, and one number higher than our node value's possible upper bounds. We do this because we want these values to be overwritten later by values that are actually within the allowed bounds of a test case. We could also choose to use negative infinity and infinity, but this is a slight optimization.
return dfs(root, -2**31-1, 2**31)