Taro Logo

Validate Binary Search Tree

Medium
Meta logo
Meta
1 view
Topics:
TreesRecursion

Given the root of a binary tree, determine if it is a valid binary search tree (BST).

A valid BST is defined as follows:

  1. The left subtree of a node contains only nodes with keys less than the node's key.
  2. The right subtree of a node contains only nodes with keys greater than the node's key.
  3. Both the left and right subtrees must also be binary search trees.

For example, consider the following trees:

Example 1:

    2
   / \
  1   3

Input: root = [2,1,3]

Output: true

Example 2:

    5
   / \
  1   4
     / \
    3   6

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.

Write a function to efficiently determine if a given binary tree is a valid BST. Explain the time and space complexity of your solution. Consider edge cases such as empty trees or trees with duplicate values. Can you provide both a naive and an optimal solution?

Solution


Naive Approach: In-order Traversal with Array Storage

A straightforward, though not the most efficient, way to approach this problem is to perform an in-order traversal of the binary tree. During this traversal, we store the node values in an array. After the traversal, we check if the array is sorted in ascending order. If it is, the tree is a valid BST; otherwise, it is not.

Algorithm

  1. Perform an in-order traversal of the binary tree.
  2. Store the value of each node visited in an array.
  3. Iterate through the array and check if it is sorted in ascending order. If any element is less than or equal to the previous element, the tree is not a valid BST.

Code (Python)

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def isValidBST_naive(root: TreeNode) -> bool:
    inorder_values = []

    def inorder_traversal(node):
        if not node:
            return
        inorder_traversal(node.left)
        inorder_values.append(node.val)
        inorder_traversal(node.right)

    inorder_traversal(root)

    for i in range(1, len(inorder_values)):
        if inorder_values[i] <= inorder_values[i - 1]:
            return False
    return True

Time and Space Complexity

  • Time Complexity: O(N), where N is the number of nodes in the tree. This is because we visit each node once during the in-order traversal and then iterate through the array of node values.
  • Space Complexity: O(N), as we store the values of all nodes in an array.

Optimal Approach: In-order Traversal with Value Comparison

The optimal approach avoids using extra space by checking the BST property during the in-order traversal itself. We keep track of the value of the previously visited node and compare it with the current node's value. If the current node's value is less than or equal to the previous node's value, the tree is not a valid BST.

Algorithm

  1. Initialize a previous variable to None.
  2. Perform an in-order traversal of the binary tree.
  3. For each node visited, check if its value is greater than the previous value. If it is not, the tree is not a valid BST.
  4. Update the previous value to the current node's value.
  5. Continue the traversal.

Code (Python)

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def isValidBST(root: TreeNode) -> bool:
    previous = None

    def inorder_traversal(node):
        nonlocal previous
        if not node:
            return True
        if not inorder_traversal(node.left):
            return False
        if previous is not None and node.val <= previous:
            return False
        previous = node.val
        return inorder_traversal(node.right)

    return inorder_traversal(root)

An alternative, and perhaps more clear, implementation utilizes a helper function that recursively validates the BST property for each node. The helper function receives a node, a minimum bound, and a maximum bound. The node's value must fall between these bounds (exclusive) for the subtree to be valid.

def isValidBST(root: TreeNode) -> bool:
    def validate(node, low=-float('inf'), high=float('inf')):
        if not node:
            return True
        if node.val <= low or node.val >= high:
            return False
        
        return (validate(node.left, low, node.val) and
                validate(node.right, node.val, high))

    return validate(root)

Time and Space Complexity

  • Time Complexity: O(N), where N is the number of nodes in the tree. This is because we visit each node once during the in-order traversal.
  • Space Complexity: O(H), where H is the height of the tree, due to the recursive call stack. In the worst case (skewed tree), H can be equal to N, resulting in O(N) space complexity. In the best case (balanced tree), H is log(N), resulting in O(log(N)) space complexity.

Edge Cases

  • Empty Tree: An empty tree is considered a valid BST.
  • Single Node Tree: A tree with only one node is also a valid BST.
  • Duplicate Values: The definition of a BST usually disallows duplicate values. The code provided reflects this assumption. If duplicates are allowed (e.g., all values in the left subtree must be less than or equal to the node's value), the comparison should be adjusted accordingly.

Summary

The optimal approach using in-order traversal with value comparison provides an efficient solution to determine if a binary tree is a valid BST, with a time complexity of O(N) and space complexity of O(H), where H is the height of the tree.