197 lines
9.4 KiB
YAML
197 lines
9.4 KiB
YAML
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
|
|
|
|
function_signature: "def lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { root: [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5], p: 2, q: 8 }
|
|
expected: 6
|
|
- input: { root: [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5], p: 2, q: 4 }
|
|
expected: 2
|
|
- input: { root: [2, 1], p: 2, q: 1 }
|
|
expected: 2
|
|
hidden:
|
|
- input: { root: [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5], p: 3, q: 5 }
|
|
expected: 4
|
|
- input: { root: [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5], p: 7, q: 9 }
|
|
expected: 8
|
|
- input: { root: [3, 1, 4, null, 2], p: 1, q: 4 }
|
|
expected: 3
|
|
- input: { root: [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5], p: 0, q: 5 }
|
|
expected: 2
|
|
- input: { root: [5, 3, 6, 2, 4], p: 2, q: 4 }
|
|
expected: 3
|
|
- input: { root: [2, 1, 3], p: 1, q: 3 }
|
|
expected: 2
|
|
|
|
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.
|