Lowest Common Ancestor in Binary Tree in DSA Go - Time & Space Complexity
We want to understand how the time needed to find the lowest common ancestor grows as the tree gets bigger.
How does the search through the tree affect the total work done?
Analyze the time complexity of the following code snippet.
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil || root == p || root == q {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil && right != nil {
return root
}
if left != nil {
return left
}
return right
}
This code finds the lowest common ancestor of two nodes in a binary tree by searching both sides recursively.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls visiting each node once.
- How many times: Each node is checked once during the recursion.
As the tree grows, the function visits every node once to find the answer.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 visits |
| 100 | About 100 visits |
| 1000 | About 1000 visits |
Pattern observation: The work grows directly with the number of nodes.
Time Complexity: O(n)
This means the time to find the lowest common ancestor grows linearly with the number of nodes in the tree.
[X] Wrong: "The function only checks a small part of the tree, so it runs faster than O(n)."
[OK] Correct: The function may need to visit every node in the worst case to find both targets, so it must check the whole tree.
Understanding this linear time complexity helps you explain why this approach is efficient and expected when working with binary trees.
"What if the tree was a binary search tree? How would the time complexity change?"
Practice
Solution
Step 1: Understand the definition of LCA
The Lowest Common Ancestor is the lowest node in the tree that has both given nodes as descendants (a node can be a descendant of itself).Step 2: Identify the deepest common ancestor
Among all common ancestors, the LCA is the one farthest from the root (deepest).Final Answer:
The deepest node that is an ancestor of both nodes -> Option CQuick Check:
LCA = deepest common ancestor [OK]
- Confusing LCA with root node always
- Thinking LCA is the node with smaller value
- Assuming LCA is a child of both nodes
Solution
Step 1: Understand pointer usage in Go for tree nodes
Tree nodes are usually passed as pointers to avoid copying and to allow modification.Step 2: Check parameter and return types
The function should accept pointers to TreeNode for root, p, and q, and return a pointer to TreeNode.Final Answer:
func lowestCommonAncestor(root *TreeNode, p *TreeNode, q *TreeNode) *TreeNode -> Option DQuick Check:
Use pointers for nodes in Go function signature [OK]
- Passing nodes by value instead of pointer
- Mixing pointer and value types in parameters
- Returning value instead of pointer
type TreeNode struct {
Val int
Left, Right *TreeNode
}
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil || root == p || root == q {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil && right != nil {
return root
}
if left != nil {
return left
}
return right
}
func main() {
root := &TreeNode{Val: 3}
root.Left = &TreeNode{Val: 5}
root.Right = &TreeNode{Val: 1}
root.Left.Left = &TreeNode{Val: 6}
root.Left.Right = &TreeNode{Val: 2}
root.Right.Left = &TreeNode{Val: 0}
root.Right.Right = &TreeNode{Val: 8}
lca := lowestCommonAncestor(root, root.Left, root.Right)
fmt.Println(lca.Val)
}Solution
Step 1: Trace recursive calls for nodes 5 and 1
Starting at root (3), left subtree contains node 5, right subtree contains node 1.Step 2: Both left and right recursive calls return non-nil nodes
Since left and right are both non-nil, root (3) is the LCA.Final Answer:
3 -> Option AQuick Check:
LCA(5,1) = 3 [OK]
- Returning one of the nodes instead of root
- Confusing node values with LCA
- Not handling nil checks properly
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil {
return nil
}
if root.Val == p.Val || root.Val == q.Val {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil {
return left
}
if right != nil {
return right
}
return root
}Solution
Step 1: Analyze return conditions after recursion
If both left and right are nil, the function incorrectly returns root instead of nil.Step 2: Understand correct base case for no ancestor found
When neither subtree contains p or q, function should return nil, not root.Final Answer:
Returns root when both left and right are nil instead of nil -> Option AQuick Check:
Return nil if no ancestor found [OK]
- Returning root instead of nil when no match
- Missing nil check at start
- Comparing values instead of pointers incorrectly
p and q?Solution
Step 1: Understand problem with duplicate values
When values are duplicated, comparing values alone can confuse nodes.Step 2: Use node pointers to uniquely identify nodes
Comparing pointers ensures the exact nodes p and q are found, not just matching values.Step 3: Modify function to compare pointers (e.g., root == p)
This guarantees correct LCA even with duplicate values.Final Answer:
Compare node pointers instead of node values to identify nodes -> Option BQuick Check:
Use pointers, not values, for unique node identity [OK]
- Comparing only values causing wrong LCA
- Assuming duplicates don't affect LCA
- Using parent pointers unnecessarily
Ready to prove it?
Pass the test and this pattern moves unknown
