Lowest Common Ancestor in Binary Tree in DSA Typescript - 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 affect the time taken?
Analyze the time complexity of the following code snippet.
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
if (!root || root === p || root === q) return root;
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root;
return left ? left : right;
}
This code finds the lowest common ancestor of two nodes in a binary tree by searching 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 number of nodes in the tree grows, the function visits each node once to check for matches.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 visits |
| 100 | About 100 visits |
| 1000 | About 1000 visits |
Pattern observation: The time grows roughly in direct proportion to the number of nodes.
Time Complexity: O(n)
This means the time to find the ancestor grows linearly with the number of nodes in the tree.
[X] Wrong: "The function only searches part of the tree, so it runs faster than visiting all nodes."
[OK] Correct: The function may need to visit every node in the worst case to find both targets, so it checks the whole tree.
Understanding this helps you explain how recursive tree searches work and why visiting all nodes is sometimes necessary.
"What if the tree was a binary search tree? How would that change the time complexity?"
Practice
Lowest Common Ancestor (LCA) in a binary tree?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: Differentiate from other nodes
It is not necessarily the root or the highest value node, but the shared ancestor closest to the two nodes.Final Answer:
The lowest node that has both given nodes as descendants -> Option BQuick Check:
LCA = lowest shared ancestor [OK]
- Confusing LCA with root node
- Thinking LCA is the node with highest value
- Assuming LCA must be a leaf node
Solution
Step 1: Analyze parameters needed for LCA
To find LCA, we need the root of the tree and the two nodes (p and q) to find the ancestor for.Step 2: Check return type
The function should return the TreeNode that is the LCA or null if not found.Final Answer:
function findLCA(root: TreeNode, p: TreeNode, q: TreeNode): TreeNode | null -> Option AQuick Check:
Correct signature includes root, p, q nodes and returns TreeNode or null [OK]
- Missing root parameter
- Using wrong parameter types like numbers instead of nodes
- Returning wrong type like number instead of TreeNode
findLCA(root, node5, node1) where node5 and node1 are nodes with values 5 and 1 respectively?class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val: number) {
this.val = val;
this.left = null;
this.right = null;
}
}
function findLCA(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
if (!root || root === p || root === q) return root;
const left = findLCA(root.left, p, q);
const right = findLCA(root.right, p, q);
if (left && right) return root;
return left ? left : right;
}
// Tree structure:
// 3
// / \
// 5 1
// / \ / \
// 6 2 0 8
// / \
// 7 4
const root = new TreeNode(3);
const node5 = new TreeNode(5);
const node1 = new TreeNode(1);
root.left = node5;
root.right = node1;
// ... other nodes assigned similarly
const lca = findLCA(root, node5, node1);
console.log(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. Both left and right recursive calls return non-null nodes.Step 2: Determine LCA based on recursive returns
Since both left and right calls return nodes, current root (3) is the LCA.Final Answer:
3 -> Option AQuick Check:
LCA of 5 and 1 is 3 [OK]
- Returning one of the nodes instead of their ancestor
- Confusing node values with node references
- Ignoring null checks in recursion
function findLCA(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
if (!root) return null;
if (root.val === p.val || root.val === q.val) return root;
const left = findLCA(root.left, p, q);
const right = findLCA(root.right, p, q);
if (left && right) return root;
return left || right;
}Solution
Step 1: Analyze node comparison logic
The function compares node values (root.val === p.val), but nodes should be compared by reference to handle duplicates correctly.Step 2: Correct the comparison to use reference equality
Change to (root === p || root === q) to compare actual nodes, not just values.Final Answer:
Change comparisons to check node references, not values -> Option CQuick Check:
Node reference comparison fixes LCA correctness [OK]
- Comparing node values instead of references
- Misusing logical operators in return
- Removing necessary recursion
p and q?Solution
Step 1: Understand the problem with duplicate values
When values repeat, comparing nodes by value can lead to wrong LCA because multiple nodes share the same value.Step 2: Use node references to uniquely identify nodes
Comparing nodes by their references (actual objects) ensures the correct nodes are identified regardless of value duplicates.Final Answer:
Use node references (memory addresses) for comparison -> Option DQuick Check:
Reference comparison handles duplicates correctly [OK]
- Comparing only node values
- Assuming smaller value node is LCA
- Ignoring duplicates in tree
Ready to prove it?
Pass the test and this pattern moves unknown
