Validate Binary Search Tree

Difficulty: Medium

You're given the root of a binary tree. Determine whether it's a valid binary search tree (BST): every node's value must be strictly greater than every value in its left subtree and strictly less than every value in its right subtree — not just its immediate children, but the entire subtree beneath it.

Examples

Input: root = [2,1,3]
Output: true

1 < 2 < 3, and the property holds everywhere.

Input: root = [10,5,15,null,null,6,20]
Output: false

6 sits in 10's right subtree (as the left child of 15), but 6 is less than 10. Comparing 6 only to its parent 15 would miss this — the whole right subtree must stay greater than 10.

Input: root = [5,4,6,null,null,3,7]
Output: false

3 is in 5's right subtree (under 6), but 3 is less than 5.

Constraints

  • The number of nodes is in the range [1, 10^4].

  • Node values are in the range [-2^31, 2^31 - 1].

Approach

The key mistake is checking a node only against its immediate children — that misses violations that come from further up the tree. Instead, each node must fall within a valid range determined by its ancestors: a left child must be less than its parent and still within whatever upper bound the parent inherited, and similarly for right children.

A different way to see the same idea: an in-order traversal (left, node, right) of a valid BST visits values in strictly increasing order. So checking that the in-order sequence never goes flat or backwards is an equally valid test.

Solutions

Brute Force — In-Order Traversal into an Array

Collect every value via an in-order traversal (left, node, right). If the tree really is a valid BST, this sequence must come out strictly increasing. So after collecting it, just walk through and confirm each value is bigger than the one before it.

class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function isValidBST(root) {
  const values = [];

  function inorder(node) {
    if (node === null) return;
    inorder(node.left);
    values.push(node.val);
    inorder(node.right);
  }

  inorder(root);

  for (let i = 1; i < values.length; i++) {
    if (values[i] <= values[i - 1]) return false;
  }
  return true;
}

Time: O(n) · Space: O(n) — storing every value in an array

Optimal — Recursive Range Checking

Give every node a valid range (lower, upper) it must fall strictly inside, inherited from its ancestors. The root's range is unbounded. When you move into a left child, that child's upper bound tightens to the parent's value (it must stay less than the parent). When you move into a right child, its lower bound tightens to the parent's value instead. If any node ever falls outside its inherited range, the tree is invalid.

class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function isValidBST(root) {
  function validate(node, lower, upper) {
    if (node === null) return true;
    if (lower !== null && node.val <= lower) return false;
    if (upper !== null && node.val >= upper) return false;

    return validate(node.left, lower, node.val) && validate(node.right, node.val, upper);
  }

  return validate(root, null, null);
}

Time: O(n) · Space: O(h) — recursion stack, where h is the tree's height