title: Lowest Common Ancestor of a Binary Search Tree slug: lowest-common-ancestor-of-a-binary-search-tree difficulty: medium leetcode_id: 235 leetcode_url: https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ categories: - trees patterns: - tree-traversal - binary-search description: | Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST. According to the definition of LCA on Wikipedia: "The lowest common ancestor is defined between two nodes `p` and `q` as the lowest node in `T` that has both `p` and `q` as descendants (where we allow **a node to be a descendant of itself**)." constraints: | - The number of nodes in the tree is in the range `[2, 10^5]` - `-10^9 <= Node.val <= 10^9` - All `Node.val` are **unique** - `p != q` - `p` and `q` will exist in the BST examples: - input: "root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8" output: "6" explanation: "The LCA of nodes 2 and 8 is 6." - input: "root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4" output: "2" explanation: "The LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition." - input: "root = [2,1], p = 2, q = 1" output: "2" explanation: "The LCA of nodes 2 and 1 is 2." explanation: intuition: | The key insight is to **leverage the BST property**: for any node, all values in its left subtree are smaller, and all values in its right subtree are larger. Think of it like searching for a meeting point. Imagine you're standing at the root, and two people are trying to find each other — one at node `p` and one at node `q`. As you traverse down the tree, at some point you'll reach a node where the two people would need to go in **different directions** to reach their respective nodes. That splitting point is the LCA. More concretely: - If both `p` and `q` are **smaller** than the current node, the LCA must be in the left subtree - If both `p` and `q` are **larger** than the current node, the LCA must be in the right subtree - If `p` and `q` are on **opposite sides** (or one equals the current node), then the current node is the LCA This is fundamentally different from finding the LCA in a general binary tree, where you'd need to search both subtrees. The BST ordering gives us a guaranteed direction at each step. approach: | We solve this using the **BST Property** to guide our traversal: **Step 1: Start at the root** - Begin traversal at the root node - We'll move down the tree based on how `p` and `q` compare to the current node   **Step 2: Compare values and decide direction** - If both `p.val` and `q.val` are **less than** `current.val`, move to the left child - If both `p.val` and `q.val` are **greater than** `current.val`, move to the right child - Otherwise, we've found the split point — return the current node   **Step 3: The split point is the LCA** - When `p` and `q` lie on different sides of the current node (or one of them equals the current node), the current node is the lowest common ancestor - Return this node as the answer   This works because the BST property guarantees that once `p` and `q` "split" to different subtrees, they can never reunite at a lower node. common_pitfalls: - title: Ignoring the BST Property description: | A common mistake is treating this like a general binary tree LCA problem and recursing into both subtrees to find `p` and `q`. In a general binary tree, you'd need to search both children and check which subtree contains which node. But in a BST, you can determine which direction to go with a simple value comparison — O(1) per node instead of potentially visiting both subtrees. This makes the BST solution O(h) instead of O(n). wrong_approach: "Search both subtrees like in a general binary tree" correct_approach: "Use value comparisons to choose one direction at each step" - title: Forgetting a Node Can Be Its Own Ancestor description: | The problem states that a node can be a descendant of itself. If `p = 2` and `q = 4`, and node 2 is an ancestor of node 4 in the BST, then the LCA is 2, not some parent of 2. When checking the split condition, remember to handle the case where the current node equals `p` or `q`. In this case, the current node is the LCA because one node is an ancestor of the other. wrong_approach: "Only return when p and q are on opposite sides" correct_approach: "Return when p and q split OR when current equals p or q" - title: Incorrect Comparison Logic description: | Be careful with the comparison operators. The condition for moving left is when **both** values are less than current. Similarly for moving right. A common bug is using OR instead of AND: - Wrong: `if p.val < current.val or q.val < current.val` (might miss the split) - Correct: `if p.val < current.val and q.val < current.val` wrong_approach: "Using OR logic for direction decisions" correct_approach: "Using AND logic — both must be less/greater to continue" key_takeaways: - "**Exploit BST ordering**: The BST property lets you make O(1) direction decisions, avoiding the need to search both subtrees" - "**Split point = LCA**: The moment two values would need to go different directions, you've found their common ancestor" - "**Iterative vs recursive**: Both approaches work, but iterative uses O(1) space vs O(h) for the recursive call stack" - "**Foundation for harder problems**: This pattern extends to problems like finding paths between nodes or validating BST structure" time_complexity: "O(h) where h is the height of the tree. In a balanced BST, h = log(n). In the worst case (skewed tree), h = n." space_complexity: "O(1) for the iterative solution. We only use a single pointer to traverse the tree, regardless of input size." solutions: - approach_name: Iterative Traversal 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 lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: # Start at the root and traverse down current = root while current: # Both nodes are in the left subtree if p.val < current.val and q.val < current.val: current = current.left # Both nodes are in the right subtree elif p.val > current.val and q.val > current.val: current = current.right # Split point found — p and q are on different sides # (or one of them equals current) else: return current return None # Should never reach here if p and q exist in tree explanation: | **Time Complexity:** O(h) — We traverse at most the height of the tree, making one comparison per level. **Space Complexity:** O(1) — Only a single pointer variable is used; no recursion stack. We exploit the BST property to navigate directly to the LCA. At each node, we compare both `p` and `q` values to decide whether to go left, right, or stop. The moment they would diverge (or one matches the current node), we've found the LCA. - approach_name: Recursive Traversal is_optimal: false code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: # Both nodes are in the left subtree if p.val < root.val and q.val < root.val: return lowest_common_ancestor(root.left, p, q) # Both nodes are in the right subtree if p.val > root.val and q.val > root.val: return lowest_common_ancestor(root.right, p, q) # Split point — this is the LCA return root explanation: | **Time Complexity:** O(h) — Same traversal pattern as iterative, visiting at most h nodes. **Space Complexity:** O(h) — Recursive call stack can grow up to the height of the tree. This recursive version follows the same logic but uses the call stack instead of a loop. While elegant, it uses more space than the iterative approach. The recursive calls naturally unwind once we find the split point.