Lowest Common Ancestor in Binary Tree in DSA Rust - Time & Space Complexity
We want to understand how the time needed to find the lowest common ancestor changes as the tree grows.
How does the search through the tree scale with its size?
Analyze the time complexity of the following code snippet.
fn lowest_common_ancestor(root: &Option>, p: i32, q: i32) -> Option<&Box> {
if let Some(node) = root {
if node.val == p || node.val == q {
return Some(node);
}
let left = lowest_common_ancestor(&node.left, p, q);
let right = lowest_common_ancestor(&node.right, p, q);
if left.is_some() && right.is_some() {
return Some(node);
}
left.or(right)
} else {
None
}
}
This code searches the binary tree to find the lowest common ancestor of two nodes by checking left and right subtrees recursively.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls visiting each node once.
- How many times: Each node is visited once during the recursion.
As the tree grows, the function visits each node once, so the work grows directly with the number of nodes.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 visits |
| 100 | About 100 visits |
| 1000 | About 1000 visits |
Pattern observation: The operations grow linearly with the number of nodes.
Time Complexity: O(n)
This means the time to find the lowest common ancestor grows in direct proportion to the number of nodes in the tree.
[X] Wrong: "The function only visits a small part of the tree, so it runs faster than O(n)."
[OK] Correct: The function may need to check all nodes in the worst case, especially if the nodes are deep or spread out, so it visits every node once.
Understanding this time complexity helps you explain how recursive tree searches work and shows you can reason about algorithms that visit all parts of a data structure.
"What if the tree was a binary search tree and you used its ordering to skip branches? How would the time complexity change?"
Practice
Solution
Step 1: Understand the definition of LCA
The Lowest Common Ancestor is the deepest node in the tree that has both given nodes as descendants (a node can be a descendant of itself).Step 2: Identify the correct description
Among the options, only the deepest common ancestor matches this definition.Final Answer:
The deepest node that is an ancestor of both nodes -> Option CQuick Check:
LCA = deepest common ancestor [OK]
- Confusing LCA with the root node
- Choosing node based on value instead of position
- Selecting node based on traversal order
Solution
Step 1: Analyze the function parameters and return type
The root is an optional boxed node reference because the tree can be empty. The nodes p and q are references to TreeNode. The return type is an optional reference to TreeNode because LCA might not exist.Step 2: Match the correct signature
fn lowest_common_ancestor(root: &Option, p: &TreeNode, q: &TreeNode) -> Option<&TreeNode> matches this pattern exactly with references and Option types.Final Answer:
fn lowest_common_ancestor(root: &Option, p: &TreeNode, q: &TreeNode) -> Option<&TreeNode> -> Option AQuick Check:
Correct Rust signature uses references and Option<Box> [OK]
- Passing TreeNode by value instead of reference
- Returning owned TreeNode instead of reference
- Using mutable references unnecessarily
struct TreeNode {
val: i32,
left: Option,
right: Option,
}
// Tree structure:
// 3
// / \
// 5 1
// / \ / \
// 6 2 0 8
// Calling lowest_common_ancestor(root, &p, &q) returns node with value:
let lca = lowest_common_ancestor(&root, &p, &q);
println!("{}", lca.unwrap().val);Solution
Step 1: Visualize the tree and nodes p and q
Node p=5 is left child of root 3, node q=1 is right child of root 3.Step 2: Find the lowest common ancestor
The lowest node that has both 5 and 1 as descendants is the root node 3.Final Answer:
3 -> Option BQuick Check:
LCA(5,1) = 3 [OK]
- Choosing one of the nodes instead of their ancestor
- Confusing child nodes as LCA
- Ignoring tree structure
fn lowest_common_ancestor(root: &Option, p: &TreeNode, q: &TreeNode) -> Option<&TreeNode> {
if root.is_none() { return None; }
let node = root.as_ref().unwrap();
if node.val == p.val || node.val == q.val {
return Some(node);
}
let left = lowest_common_ancestor(&node.left, p, q);
let right = lowest_common_ancestor(&node.right, p, q);
if left.is_some() && right.is_some() {
return Some(node);
}
if left.is_some() {
return left;
}
right;
}Solution
Step 1: Trace the control flow paths
The function handles empty root, current node matching p/q, and both subtrees containing descendants.Step 2: Analyze the final path
When neither both-some nor left-some conditions trigger (left None), it reachesright;. The semicolon turns it into a statement that evaluates right but discards it, ending the function without returning a value. Thus, missing return statement.Final Answer:
Missing return statement in last line -> Option AQuick Check:
right; discards result instead of returning [OK]
- Adding semicolon to last line discarding result
- Thinking unwrap causes panic without checking is_none
- Confusing ownership with references
Solution
Step 1: Understand the problem with duplicates
When nodes have duplicate values, comparing by value is ambiguous and can return wrong LCA.Step 2: Use node references for comparison
Comparing nodes by their memory addresses or references ensures exact node identity, avoiding confusion from duplicates.Step 3: Choose the correct approach
Compare nodes by their memory addresses or references instead of values correctly suggests comparing nodes by references, which is the safe and correct method in Rust.Final Answer:
Compare nodes by their memory addresses or references instead of values -> Option DQuick Check:
Use references, not values, to handle duplicates [OK]
- Comparing nodes by value causing wrong LCA
- Trying to convert tree to BST unnecessarily
- Returning first matching value node ignoring duplicates
Ready to prove it?
Pass the test and this pattern moves unknown
