title: Validate Binary Search Tree
slug: validate-binary-search-tree
difficulty: medium
leetcode_id: 98
leetcode_url: https://leetcode.com/problems/validate-binary-search-tree/
categories:
  - trees
  - recursion
patterns:
  - dfs
  - tree-traversal

function_signature: "def is_valid_bst(root: TreeNode | None) -> bool:"

test_cases:
  visible:
    - input: { root: [2, 1, 3] }
      expected: true
    - input: { root: [5, 1, 4, null, null, 3, 6] }
      expected: false
    - input: { root: [1] }
      expected: true
  hidden:
    - input: { root: [5, 4, 6, null, null, 3, 7] }
      expected: false
    - input: { root: [2, 2, 2] }
      expected: false
    - input: { root: [10, 5, 15, null, null, 6, 20] }
      expected: false

description: |
  Given the `root` of a binary tree, determine if it is a valid **binary search tree (BST)**.

  A valid BST is defined as follows:

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

constraints: |
  - The number of nodes in the tree is in the range `[1, 10^4]`
  - `-2^31 <= Node.val <= 2^31 - 1`

examples:
  - input: "root = [2,1,3]"
    output: "true"
    explanation: "Left child 1 < root 2 < right child 3. Valid BST."
  - input: "root = [5,1,4,null,null,3,6]"
    output: "false"
    explanation: "The root's right child is 4, which is less than root 5. Even though 4's children (3, 6) satisfy local constraints, 3 should be > 5 to be in the right subtree. Invalid BST."

explanation:
  intuition: |
    The naive approach is to check if each node satisfies `left.val < node.val < right.val`. But this only checks **local** constraints. BST requires **global** constraints!

    Think of it like this: every node in the right subtree must be greater than the root — not just the immediate right child. In `[5,1,4,null,null,3,6]`, the value 3 is correctly less than its parent 4, but it's in the right subtree of 5, so it should be greater than 5!

    The key insight is to pass down **valid ranges** as we traverse. When we go left, we tighten the upper bound. When we go right, we tighten the lower bound.

    For example, starting at root 5:
    - Left subtree must have values in `(-∞, 5)`
    - Right subtree must have values in `(5, +∞)`
    - In the right subtree, going left to node 3 requires values in `(5, 4)` — but 3 < 5, so it fails!

  approach: |
    We solve this using **DFS with Range Validation**:

    **Step 1: Define the recursive validation function**

    - `validate(node, min_val, max_val)` returns True if the subtree rooted at `node` is a valid BST within the range `(min_val, max_val)`
    - Start with the root and range `(-∞, +∞)`

    &nbsp;

    **Step 2: Base case**

    - If `node` is None, return True (empty tree is valid)

    &nbsp;

    **Step 3: Check the current node**

    - If `node.val <= min_val` or `node.val >= max_val`, return False
    - The node violates its required range

    &nbsp;

    **Step 4: Recurse with updated ranges**

    - Validate left subtree with range `(min_val, node.val)` — must be less than current node
    - Validate right subtree with range `(node.val, max_val)` — must be greater than current node
    - Return True only if both subtrees are valid

    &nbsp;

    This ensures every node satisfies constraints from all its ancestors, not just its parent.

  common_pitfalls:
    - title: Only Checking Immediate Parent-Child Relationships
      description: |
        Checking `left.val < node.val` and `node.val < right.val` only validates local constraints. A node deep in a subtree might violate constraints from ancestors.

        For example, in `[5,1,4,null,null,3,6]`, node 3 is correctly less than its parent 4, but it's in the right subtree of 5 and should be greater than 5.
      wrong_approach: "Only checking node.left.val < node.val < node.right.val"
      correct_approach: "Pass min/max bounds down through recursion"

    - title: Using Integer Min/Max as Initial Bounds
      description: |
        Node values can be at integer boundaries (`-2^31` to `2^31-1`). Using `-2^31` as the initial lower bound would reject a valid node with that value.

        Use `float('-inf')` and `float('inf')` or `None` with special handling.
      wrong_approach: "validate(root, -2**31, 2**31-1)"
      correct_approach: "validate(root, float('-inf'), float('inf'))"

    - title: Using Non-Strict Comparisons
      description: |
        The BST definition requires **strictly** less than and **strictly** greater than. Equal values are not allowed.

        Use `<` and `>`, not `<=` and `>=`.
      wrong_approach: "if node.val <= max_val and node.val >= min_val"
      correct_approach: "if min_val < node.val < max_val"

  key_takeaways:
    - "**BST is a global property**: Every node must satisfy constraints from ALL ancestors, not just its parent"
    - "**Range propagation**: Pass valid ranges down during recursion to enforce global constraints"
    - "**Inorder traversal alternative**: BST's inorder traversal produces a strictly increasing sequence"
    - "**Handle boundary values**: Use infinity or None for initial bounds to handle edge cases"

  time_complexity: "O(n). We visit each node exactly once."
  space_complexity: "O(h). Recursion stack depth equals tree height — O(log n) for balanced trees, O(n) for skewed trees."

solutions:
  - approach_name: DFS with Range Validation
    is_optimal: true
    code: |
      class TreeNode:
          def __init__(self, val=0, left=None, right=None):
              self.val = val
              self.left = left
              self.right = right

      def is_valid_bst(root: TreeNode | None) -> bool:
          def validate(node: TreeNode | None, min_val: float, max_val: float) -> bool:
              # Empty tree is valid
              if not node:
                  return True

              # Check if current node violates its range
              if node.val <= min_val or node.val >= max_val:
                  return False

              # Validate subtrees with tightened bounds
              # Left subtree: values must be < node.val
              # Right subtree: values must be > node.val
              return (validate(node.left, min_val, node.val) and
                      validate(node.right, node.val, max_val))

          # Start with infinite bounds
          return validate(root, float('-inf'), float('inf'))
    explanation: |
      **Time Complexity:** O(n) — Visit each node once.

      **Space Complexity:** O(h) — Recursion stack depth (tree height).

      We pass valid ranges down the tree. Going left tightens the upper bound (must be less than parent). Going right tightens the lower bound (must be greater than parent). Each node is checked against accumulated constraints from all ancestors.

  - approach_name: Inorder Traversal
    is_optimal: true
    code: |
      def is_valid_bst(root: TreeNode | None) -> bool:
          prev = float('-inf')

          def inorder(node: TreeNode | None) -> bool:
              nonlocal prev

              if not node:
                  return True

              # Traverse left subtree
              if not inorder(node.left):
                  return False

              # Check current node against previous value
              if node.val <= prev:
                  return False
              prev = node.val

              # Traverse right subtree
              return inorder(node.right)

          return inorder(root)
    explanation: |
      **Time Complexity:** O(n) — Visit each node once.

      **Space Complexity:** O(h) — Recursion stack depth.

      Inorder traversal of a valid BST produces a strictly increasing sequence. We track the previous value and ensure each node is greater than it. If any node fails this check, the tree is not a valid BST.
