Skip to main content

Validate Binary Search Tree

Given a binary tree, containing unique values, determine if it is a valid binary search tree.

Note: The invariants of a binary search tree (in our case) are:

  • All values to the left of a given node are less than the current node's value
  • All values to the right of a given node are greater than the current node's value
  • Both the left and right subtrees of a given node must also be binary search trees

Example(s)

Example 1:

Input:
1
/ \
2 3

Output: false
Explanation:
Node 1 has value 1
Left child has value 2, which is greater than 1
This violates the BST property (left should be less)

Example 2:

Input:
2
/ \
1 3

Output: true
Explanation:
Node 2 has value 2
Left child (1) < 2 ✓
Right child (3) > 2 ✓
Both subtrees are valid BSTs

Example 3:

Input:
5
/ \
1 4
/ \
3 6

Output: false
Explanation:
Node 5 has value 5
Right child 4 < 5 ✗ (should be greater)
This violates the BST property

Example 4:

Input:
10
/ \
5 15
/ \
6 20

Output: false
Explanation:
Node 10 has value 10
Right subtree: 15 is valid
But 15's left child is 6, which is less than 10 (ancestor)
This violates the BST property (all right descendants must be > 10)

Solution

The solution uses DFS with Bounds Checking:

  1. Bounds tracking: For each node, track min and max allowed values
  2. Recursive validation: Check if node value is within bounds
  3. Update bounds: For left subtree, update max; for right subtree, update min
  4. Base case: Null nodes are valid BSTs

JavaScript Solution - DFS with Bounds

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
*     this.val = (val===undefined ? 0 : val)
*     this.left = (left===undefined ? null : left)
*     this.right = (right===undefined ? null : right)
* }
*/

/**
* Check if binary tree is a valid BST
* @param {TreeNode} root - Root of binary tree
* @return {boolean} - Whether tree is a valid BST
*/
function isValidBST(root) {
/**
 * Helper function to validate with bounds
 * @param {TreeNode} node - Current node
 * @param {number} min - Minimum allowed value
 * @param {number} max - Maximum allowed value
 * @return {boolean} - Whether subtree is valid BST
 */
function validate(node, min, max) {
  // Base case: null nodes are valid
  if (!node) return true;
  
  // Check if current node value is within bounds
  if (node.val <= min || node.val >= max) {
    return false;
  }
  
  // Recursively validate left and right subtrees
  // Left subtree: all values must be < node.val, so max = node.val
  // Right subtree: all values must be > node.val, so min = node.val
  return validate(node.left, min, node.val) && 
         validate(node.right, node.val, max);
}

// Start with unbounded range (use Infinity for bounds)
return validate(root, -Infinity, Infinity);
}

// Helper function to create tree nodes for testing
function TreeNode(val, left, right) {
this.val = val;
this.left = left;
this.right = right;
}

// Test cases
// Example 1: [1, 2, 3]
const tree1 = new TreeNode(1,
new TreeNode(2),
new TreeNode(3)
);
console.log('Example 1:', isValidBST(tree1)); // false

// Example 2: [2, 1, 3]
const tree2 = new TreeNode(2,
new TreeNode(1),
new TreeNode(3)
);
console.log('Example 2:', isValidBST(tree2)); // true

// Example 3: [5, 1, 4, null, null, 3, 6]
const tree3 = new TreeNode(5,
new TreeNode(1),
new TreeNode(4, new TreeNode(3), new TreeNode(6))
);
console.log('Example 3:', isValidBST(tree3)); // false
Output:
Click "Run Code" to execute the code and see the results.

Alternative Solution (In-Order Traversal)

Here's an alternative using in-order traversal (BST in-order gives sorted order):

/**
* Alternative: In-order traversal
* BST in-order traversal gives sorted order
*/
function isValidBSTInOrder(root) {
let prev = null;

function inOrder(node) {
if (!node) return true;

// Traverse left subtree
if (!inOrder(node.left)) return false;

// Check current node
if (prev !== null && node.val <= prev) {
return false;
}
prev = node.val;

// Traverse right subtree
return inOrder(node.right);
}

return inOrder(root);
}

Complexity

  • Time Complexity: O(n) - Where n is the number of nodes in the tree. We visit each node once.
  • Space Complexity: O(h) - Where h is the height of the tree. For the recursion stack. In worst case (skewed tree), O(n). In average case (balanced tree), O(log n).

Approach

The solution uses DFS with Bounds Checking:

  1. Bounds tracking:

    • For each node, maintain min and max allowed values
    • Initially, use -Infinity and Infinity (unbounded)
  2. Validation check:

    • Current node value must be: min < node.val < max
    • If not, return false (violates BST property)
  3. Recursive validation:

    • Left subtree: All values must be < current node
      • Update max bound to node.val
      • Keep min bound unchanged
    • Right subtree: All values must be > current node
      • Update min bound to node.val
      • Keep max bound unchanged
  4. Base case:

    • Null nodes are valid BSTs (empty tree is valid)

Key Insights

  • Bounds checking: Track min and max allowed values for each node
  • BST property: Left < node < right, recursively
  • Ancestor constraint: All descendants must satisfy ancestor bounds
  • O(n) time: Must visit all nodes
  • O(h) space: Recursion stack depth

Step-by-Step Example

Let's trace through Example 1:

Tree:
1
/ \
2 3

isValidBST(root=1):
validate(1, -Infinity, Infinity):
node.val=1, min=-Infinity, max=Infinity
Check: -Infinity < 1 < Infinity ✓
validate(2, -Infinity, 1):
node.val=2, min=-Infinity, max=1
Check: -Infinity < 2 < 1 ✗ (2 >= 1)
Return false ✗
Return false

Result: false

Let's trace through Example 2:

Tree:
2
/ \
1 3

isValidBST(root=2):
validate(2, -Infinity, Infinity):
node.val=2, min=-Infinity, max=Infinity
Check: -Infinity < 2 < Infinity ✓
validate(1, -Infinity, 2):
node.val=1, min=-Infinity, max=2
Check: -Infinity < 1 < 2 ✓
validate(null, -Infinity, 1) → true
validate(null, 1, 2) → true
Return true ✓
validate(3, 2, Infinity):
node.val=3, min=2, max=Infinity
Check: 2 < 3 < Infinity ✓
validate(null, 2, 3) → true
validate(null, 3, Infinity) → true
Return true ✓
Return true && true = true ✓

Result: true

Visual Representation

Example 1: Not a BST
1
/ \
2 3

Validation:
Node 1: (-∞, +∞) → 1 is valid ✓
Node 2: (-∞, 1) → 2 >= 1 ✗
Result: false ✗

Example 2: Valid BST
2
/ \
1 3

Validation:
Node 2: (-∞, +∞) → 2 is valid ✓
Node 1: (-∞, 2) → 1 < 2 ✓
Node 3: (2, +∞) → 3 > 2 ✓
Result: true ✓

Example 4: Not a BST
10
/ \
5 15
/ \
6 20

Validation:
Node 10: (-∞, +∞) → 10 is valid ✓
Node 5: (-∞, 10) → 5 < 10 ✓
Node 15: (10, +∞) → 15 > 10 ✓
Node 6: (10, 15) → 6 <= 10 ✗ (should be > 10)
Result: false ✗

Edge Cases

  • Empty tree: Return true (empty tree is valid BST)
  • Single node: Return true (single node is valid BST)
  • All left children: Must be decreasing
  • All right children: Must be increasing
  • Large values: Use Infinity for bounds

Important Notes

  • Strict inequality: Left &lt; node &lt; right (not <= or >=)
  • Bounds propagation: Bounds are inherited from ancestors
  • Ancestor constraint: All descendants must satisfy all ancestor bounds
  • O(n) time: Must visit all nodes
  • O(h) space: Recursion stack depth

Why Bounds Checking Works

Key insight:

  • A BST must satisfy: left < node < right
  • But this is not enough! We also need:
    • All left descendants < node
    • All right descendants > node
    • All descendants must satisfy ancestor bounds
  • Bounds checking ensures all these constraints are satisfied

Example:

  • Tree: 10 → 15 → 6
  • Node 6 is in right subtree of 10, so must be > 10
  • But 6 < 10, so invalid
  • Bounds checking catches this: node 6 has bounds (10, 15), but 6 <= 10
  • Search in BST: Search in valid BST
  • Insert into BST: Insert maintaining BST property
  • Delete Node in BST: Delete maintaining BST property
  • Kth Smallest Element in BST: Use BST property

Takeaways

  • Bounds checking ensures all BST constraints
  • Ancestor bounds must be satisfied by all descendants
  • O(n) time to visit all nodes
  • O(h) space for recursion stack
  • Classic tree problem with important constraints