Framework Thinking - Neetcode 150 - Lowest Common Ancestor in Binary Search Tree
Here is solutions for Lowest Common Ancestor in Binary Search Tree.
1. Understand the problem
- You are given a Binary Search Tree (BST) and two nodes p and q. You must find their Lowest Common Ancestor (LCA).
Definition:
- The LCA of two nodes p and q is the lowest (deepest) node in the tree that has both p and q as descendants
Because this is a BST, it satisfies:
left subtree < root < right subtree
2. Clarify constraints, asks 4 - 5 questions including edge cases.
- Are both p and q guaranteed to exist in the tree?
- Yes
- Can one node be the ancestor of the other?
- Yes → then that node is the LCA
- Is the tree guaranteed to be a valid BST (no duplicates)?
- Yes, strict BST
- What is the max size and height of the tree?
- Up to 10^5 nodes; skewed tree possible
- Values range? Can be negative?
- Yes, arbitrary integers
Edge cases:
- p == q
- root == p or root == q
- Completely skewed tree (like a linked list)
3. Explore examples.
- Example 1:
6
/ \
2 8
/ \ / \
0 4 7 9
/ \
3 5
p = 2, q = 8
LCA = 6
- Example 2
p = 2, q = 4
LCA = 2 (because 2 is ancestor of 4)
4. Brainstorm 2 - 3 solutions, naive solution first and optimize later. Explain the key idea of each solution.
4.1. Solution 1 — Naive (Convert to paths) - Time O(N), Space O(N)
-
Idea:
-
Find path from root -> p
-
Find path from root -> q
-
Compare paths to find last common node
-
-
Time: O(N)
-
Space: O(N)
4.2. Solution 2 - Binary Tree Idea — DFS on BST (Optimal Recursive) - Time O(H), worst case O(N), best case O(logN), Space O(N)
-
Key BST Insight:
-
If p and q are both smaller than root -> go left
-
If both are greater than root -> go right
-
Otherwise -> root is the LCA -> Idea chỗ này, vì nó cũng là số gần nhất với left and right.
-
p < root < q → root is LCA
-
Time: O(H), worst case O(N), best case O(logN) Solution 2 — DFS on BST (Optimal Recursive) Key BST Insight:
-
Space: O(H) recursion stack
4.3. Iterative BST (Best for interviews) - Time O(H), Space O(1)
Same logic as Solution 2 but no recursion.
-
Time: O(H)
-
Space: O(1), using only a curr pointer to become root and compare curr.val with l.val and r.val.
5. Implement solutions.
5.1. Naive Solution - Time O(N), Space O(N)
def lowestCommonAncestor(self, root, p, q):
path_p = []
path_q = []
self.findPath(root, p, path_p)
self.findPath(root, q, path_q)
# Keep track the node.
i = 0
while i < len(path_p) and i < len(path_q) and path_p[i] == path_q[i]:
i += 1
return path_p[i - 1] # ✅ returns NODE
def findPath(self, root, target, path):
if not root:
return False # ✅ boolean
path.append(root)
if root == target:
return True # ✅ boolean
if self.findPath(root.left, target, path) or \
self.findPath(root.right, target, path):
return True # ✅ boolean
path.pop()
return False # ✅ boolean
-
Time: O(N)
-
Space: O(N)
5.2. Recursion BST - Time O(H), Space O(H).
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
if not root or not p or not q:
return None
if (max(p.val, q.val) < root.val):
return self.lowestCommonAncestor(root.left, p, q)
elif (min(p.val, q.val) > root.val):
return self.lowestCommonAncestor(root.right, p, q)
else:
return root
-
Time: O(H),
-
Space: O(H).
5.3. Iterative BST - Time O(H), Space O(1).
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
cur = root
while cur:
if p.val > cur.val and q.val > cur.val:
cur = cur.right
elif p.val < cur.val and q.val < cur.val:
cur = cur.left
else:
return cur
-
Time: O(H),
-
Space: O(1).
6. Dry run testcases.
6
/ \
2 8
/ \ / \
0 4 7 9
/ \
3 5
p = 2, q = 4
Dry run:
cur = 6
2 < 6 and 4 < 6 => True
=> cur = cur.left = 2
---
cur = 2
2 > 2 ? False
4 > 2 ? True → (AND False)
2 < 2 ? False → (AND False)
=> ELSE => return 2