Files
codetutor/backend/data/questions/validate-binary-search-tree.yaml
2025-05-30 19:18:33 +01:00

180 lines
7.3 KiB
YAML

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
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.