Validate Binary Search Tree (BST)

Difficulty: Easy, Asked-in: Microsoft, Google, Amazon, eBay, Adobe, Qualcomm, VMWare, Walmart.

Key takeaway: An excellent problem to understand the properties of binary search trees and learn step-by-step optimisation using various approaches.

Let’s understand the problem

Given the root of a binary tree, write a program to check whether it is a valid Binary Search Tree (BST) or not. A BST is valid if it has the following properties:

  • All nodes in the left subtree have values less than or equal to the root value.
  • All nodes in the right subtree have values greater than or equal to the root value.
  • Both the left and right subtrees are also binary search trees.

This is known as the BST property, which must hold true for each node in the tree, if it is a valid BST.

Examples

Check if a binary tree is BST or not example

Discussed solution approaches

  • A wrong solution approach!
  • Brute force approach by checking BST property at each node
  • Optimised version of the brute force approach
  • Using inorder traversal and extra space
  • In-place solution using inorder traversal

Wrong solution approach!

A poor understanding of this problem can lead to a wrong approach. For example, suppose we understood the BST property incorrectly and designed this solution: We visit each node recursively and compare value stored in each node with value stored on the left and right child. If the node value is less than left child value or greater than right child value, we return false. Otherwise, we recursively call the same function to verify BST for left and right sub-tree.

Pseudocode

isValidBST(root)
    if root is null
        return true

    if root.left is not null and root.left.value > root.value
        return false

    if root.right is not null and root.right.value < root.value
        return false

    leftIsValid = isValidBST(root.left)
    rightIsValid = isValidBST(root.right)

    return leftIsValid and rightIsValid

What is the mistake in above solution?

Based on the BST property, each node value must be greater than all nodes in the left sub-tree and less than all nodes in the right sub-tree. But above solution check this only for the children of each node, not the entire left or right subtree. So this solution will give the wrong result for several input cases. Consider this example:

Not a valid BST

Brute force approach by checking BST property at each node

Solution idea

Now the critical question is: How can we fix the above solution? Here is an idea: A tree can not be BST if left subtree of a node contains any value greater than node value and right subtree contains any value smaller than node value. In other words, node value must be greater than the maximum in left subtree and less than the minimum in right subtree. Note: In a BST, the rightmost node would be the node with the maximum value and the leftmost node would be the node with the minimum value.

Solution steps

  • Base case: If (root == NULL ), return true.
  • We find max in the left subtree and min in the right subtree using two helper functions i.e. leftMax = bstMax(root->left) and rightMax = bstMin(root->right).
  • If leftMax > root->value or rightMax < root->value, then root node violates the BST property and we return false. Otherwise, root node satisfies the BST property. So we recursively check that left and right subtree are correct BST or not. For this, we call the same function with root->left and root->right as input parameters.
  • If both left and right are true, return true. Otherwise, return false.

Solution pseudocode

isValidBST(root)
    if root is null
        return true

    if root.left is not null
        leftMax = bstMax(root.left)
        if leftMax > root.value
            return false

    if root.right is not null
        rightMin = bstMin(root.right)
        if rightMin < root.value
            return false

    return isValidBST(root.left) and isValidBST(root.right)

bstMax(root)
    while root.right is not null
        root = root.right

    return root.value

bstMin(root)
    while root.left is not null
        root = root.left

    return root.value

Time complexity analysis

We visit each node recursively, finding the max in the left subtree and the min in the right subtree. Here, finding the max and min in the BST are two critical extra operations and the complexity of these operations depends on the tree structure.

  • The worst-case time complexity of finding max and min in a BST = O(n). This is the situation when the tree is highly unbalanced i.e., either left-skewed (all nodes have a left child except the single leaf node) or a right-skewed tree (all nodes have a right child except the single leaf node).
  • Recurrence relation for worst case => T(n) = T(n - 1) + O(n). Worst-case time complexity = O(n^2).
  • The best-case time complexity of finding max and min in a BST = O(logn). This is the situation of the completely balanced tree. The recurrence relation for the best case => T(n) = 2T(n/2) + O(logn). Proof that the best case time complexity is O(n)! Hint: Use the first case of the master theorem.

An optimised version of the brute force approach

Can we think of solving this problem efficiently? If we observe, we are performing O(n) extra operations for each node in the worst case and because of this, our worst-case time complexity is O(n^2). Now the critical question: Can we optimize the cost of extra operation to O(1)? Let's think!

Solution idea

In a BST, the values stored in the subtree rooted at any node lie within a specific range. In other words, there is an upper (rangeMax) and a lower (rangeMin) limit for the values in a particular subtree. For example:

  • The tree with the root node has values in the range (INTMIN, INTMAX).
  • The values in the left subtree are within the range (INT_MIN, root->value).
  • The values in the right subtree are within the range (root->value, INT_MAX).

One solution is to traverse each node and track min and max range of values allowed for the subtree rooted at each node. To do this, we can pass the range values as function arguments while recursing through the left and right subtrees. The initial values for the min and max range should be INTMIN and INTMAX.

During this process, if node->value < rangeMin or node->value > rangeMax, then the node value falls outside the valid range and violates the BST property. In that case, we return false. Otherwise, we recursively validate the left and right subtrees with updated ranges.

Check if a binary tree is BST or not solution using range min and max

Solution pseudocode

isBST(root)
    return isValidBST(root, INT_MIN, INT_MAX)

isValidBST(root, rangeMin, rangeMax)
    if root is null
        return true

    if root.value < rangeMin or root.value > rangeMax
        return false

    leftIsValid = isValidBST(root.left, rangeMin, root.value)
    rightIsValid = isValidBST(root.right, root.value, rangeMax)

    return leftIsValid and rightIsValid

Time and space complexity analysis

We visit each node only once and perform O(1) operation with each node. So time complexity = n* O(1) = O(n). Space complexity depends on the size of the recursion call stack, which is equal to the height of the tree. So space complexity = O(h). What would be the worst and best case of space complexity? Think!

Using inorder traversal and extra space

Solution idea

As we know, the inorder traversal of a binary search tree visits node values in sorted order. So another idea would be to traverse the tree using inorder traversal and store the node values in extra memory. Then we can traverse this extra memory using a loop to check whether the values are sorted. If they are sorted, then the tree is a BST. Otherwise, it is not.

Solution pseudocode

inorderTraversal(root, sorted)
    if root is null
        return

    inorderTraversal(root.left, sorted)
    append root.value to sorted
    inorderTraversal(root.right, sorted)

isValidBST(root)
    sorted = empty list
    inorderTraversal(root, sorted)

    for i from 1 to size(sorted) - 1
        if sorted[i] < sorted[i - 1]
            return false

    return true

Time and space complexity analysis

Time complexity = Time complexity of inorder traversal + Time complexity of the single loop to check sorted array = O(n) + O(n) = O(n). Space complexity = O(n) for storing elements in an array + O(h) for recursion stack space = O(n + h).

In-place solution using inorder traversal

Solution idea

The critical question is: Can we think to optimize the above approach and avoid using extra space? As we know that inorder traversal of a BST returns the nodes in sorted order. So one idea would be to keep track of previously visited nodes while traversing the tree. If current node value is less than previous node value, then tree is not BST.

Solution pseudocode: Recursive inorder traversal

isBST(root)
    previous = null
    return isValidBST(root, previous)


isValidBST(root, previous)
    if root is null
        return true

    if isValidBST(root.left, previous) is false
        return false

    if previous is not null and root.value < previous.value
        return false

    previous = root
    return isValidBST(root.right, previous)

Solution pseudocode: Iterative inorder traversal using stack

isValidBST(root)
    if root is null
        return true

    stack = empty stack
    current = root
    previous = null

    while current is not null or stack is not empty
        if current is not null
            push current onto stack
            current = current.left
        else
            current = pop from stack
            if previous is not null and current.value < previous.value
                return false

            previous = current
            current = current.right

    return true

Time and space complexity analysis

We visit each node only once and perform O(1) operation with each node. So time Complexity = n* O(1) = O(n). Space complexity depends on the depth of the size of the recursion call stack, which is equal to the height of the tree. So space complexity = O(h).

Critical Ideas to think!

  • In the 4th approach using recursive inorder traversal, can we think of implementing without using the pointer by reference?
  • Can we think of implementing 2nd approach, using pointers by reference instead of passing the value?
  • Is there a way to solve this problem using some other approach?

Suggested problems to practice

  • Check if the given binary tree is a strict binary tree or not
  • Check whether the given binary is perfect or not
  • Check if a binary tree is a complete tree or not
  • Check if a binary tree is a subtree of another binary subtree
  • Check if the given binary tree is Heap or not
  • Check if the given binary tree is SumTree or not

Please write in the message below if you have alternative approaches or find an error/bug in the above approaches. Enjoy learning, Enjoy algorithms!

More from EnjoyAlgorithms

Self-paced Courses and Blogs