Given the root of a binary tree and two nodes p and q present in the tree, find their Lowest Common Ancestor (LCA).
The LCA is the deepest node in the tree that is an ancestor of both p and q.
A node can also be considered an ancestor of itself. Therefore, if one of the given nodes lies on the path from the root to the other node, that node itself can be the LCA.
Example 1
Input:root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4], p = 5, q = 1
Output:3
Explanation:
Node 3 is the deepest node that is an ancestor of both nodes 5 and 1, so their Lowest Common Ancestor is 3.
Example 2
Input:root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4], p = 5, q = 4
Output:5
Explanation:
Node 5 is an ancestor of node 4. Since a node can be considered an ancestor of itself, the Lowest Common Ancestor is 5.
Approach 1
Every node in a binary tree has exactly one path from the root.
Two lists, pathP and pathQ, are maintained. pathP stores the sequence of ancestors from the root to node p, while pathQ stores the corresponding sequence from the root to node q.
Once both paths are available, they remain identical from the root until their deepest common node. After that point, either the paths move into different branches or one of them ends.
Therefore, the last matching node in pathP and pathQ is the Lowest Common Ancestor.
This approach follows the definition directly, but two complete root-to-node paths must first be found and stored.
Algorithm
A path named
pathPis constructed using DFS so that the complete ancestor chain from the root to nodepis stored.A second path named
pathQis constructed in the same way so that the ancestor chain from the root to nodeqis available.Both paths are compared from their first position because their common prefix represents the nodes that are ancestors of both targets.
Matching nodes are processed until different nodes are encountered or one of the paths ends.
The last matching node is identified as the deepest node shared by both ancestor chains.
That node is returned as the Lowest Common Ancestor.
Dry Run
Lowest Common Ancestor Approach 1 Dry Run .png
Solution
#include <bits/stdc++.h>using namespace std;struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int value) { val = value; left = nullptr; right = nullptr; }};class Solution {private: // Stores the root-to-target path // using DFS and backtracking. bool findPath( TreeNode* node, TreeNode* target, vector<TreeNode*>& path ) { if (node == nullptr) { return false; } path.push_back(node); // The required ancestor chain is complete // once the target node is reached. if (node == target) { return true; } if ( findPath(node->left, target, path) || findPath(node->right, target, path) ) { return true; } // The node is removed when the target // is not present in this subtree. path.pop_back(); return false; }public: // Finds the last common node in // the root-to-p and root-to-q paths. TreeNode* lowestCommonAncestor( TreeNode* root, TreeNode* p, TreeNode* q ) { vector<TreeNode*> pathP; vector<TreeNode*> pathQ; findPath(root, p, pathP); findPath(root, q, pathQ); TreeNode* lca = nullptr; int limit = min( pathP.size(), pathQ.size() ); // Both paths share the same prefix // until their Lowest Common Ancestor. for (int i = 0; i < limit; i++) { if (pathP[i] != pathQ[i]) { break; } lca = pathP[i]; } return lca; }};int main() { TreeNode* root = new TreeNode(3); root->left = new TreeNode(5); root->right = new TreeNode(1); root->left->left = new TreeNode(6); root->left->right = new TreeNode(2); root->right->left = new TreeNode(0); root->right->right = new TreeNode(8); root->left->right->left = new TreeNode(7); root->left->right->right = new TreeNode(4); TreeNode* p = root->left; TreeNode* q = root->left->right->right; Solution solution; TreeNode* lca = solution.lowestCommonAncestor(root, p, q); cout << lca->val << endl; return 0;}Complexity Analysis
Let N be the number of nodes in the binary tree and H be the height of the tree.
Time Complexity: O(N). Each root-to-node search may visit up to N nodes, and performing two such searches still gives O(N) overall. Comparing pathP and pathQ requires at most O(H) additional time.
Space Complexity: O(H), where H is the height of the binary tree. The recursive call stack and the stored root-to-node paths can each contain at most H nodes. For a skewed tree, this can become O(N).
Approach 2
If every node knew its parent, the search could be performed upward instead of repeatedly moving downward from the root.
A mapping of:
node → parent
is first constructed.
A queue named nodesQueue is used for a level-order traversal because it stores the nodes whose children still need to be processed while the parent relationships are being built.
After the mapping has been constructed, an ancestors set is used to store every node encountered while moving from p toward the root. The set is chosen because it allows constant-time average membership checks when the ancestor chain of q is later examined.
Node q is then moved upward through its parent links. The first node encountered that already exists in ancestors is the deepest node shared by both ancestor chains and is therefore the LCA.
Algorithm
A parent mapping is constructed using a queue-based level-order traversal (BFS) so that every visited child is associated with its parent.
A queue named
nodesQueueis maintained because it stores nodes whose children still need to be examined while the parent mapping is being created.BFS is continued until parent information for both
pandqhas been discovered.A set named
ancestorsis then created to store all nodes encountered while moving upward fromp, allowing nodes in this ancestor chain to be checked efficiently.Starting from
p, parent links are repeatedly followed and every visited node is inserted intoancestors.Starting from
q, parent links are followed upward until a node already present inancestorsis encountered.The first such node is returned because it is the lowest ancestor shared by both target nodes.
Dry Run
Lowest Common Ancestor Approach 2 Dry Run .png
Solution
#include <bits/stdc++.h>using namespace std;struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int value) { val = value; left = nullptr; right = nullptr; }};class Solution {public: // Builds parent relationships using BFS // and compares the ancestor chains. TreeNode* lowestCommonAncestor( TreeNode* root, TreeNode* p, TreeNode* q ) { unordered_map<TreeNode*, TreeNode*> parent; // nodesQueue stores nodes whose children // still need parent information. queue<TreeNode*> nodesQueue; parent[root] = nullptr; nodesQueue.push(root); // BFS continues until both target nodes // have known parent relationships. while ( parent.find(p) == parent.end() || parent.find(q) == parent.end() ) { TreeNode* node = nodesQueue.front(); nodesQueue.pop(); if (node->left != nullptr) { parent[node->left] = node; nodesQueue.push(node->left); } if (node->right != nullptr) { parent[node->right] = node; nodesQueue.push(node->right); } } // ancestors stores every node on // p's path from itself to the root. unordered_set<TreeNode*> ancestors; TreeNode* current = p; while (current != nullptr) { ancestors.insert(current); current = parent[current]; } current = q; // The first node from q's ancestor chain // already seen from p is the LCA. while ( ancestors.find(current) == ancestors.end() ) { current = parent[current]; } return current; }};int main() { TreeNode* root = new TreeNode(3); root->left = new TreeNode(5); root->right = new TreeNode(1); root->left->left = new TreeNode(6); root->left->right = new TreeNode(2); root->right->left = new TreeNode(0); root->right->right = new TreeNode(8); root->left->right->left = new TreeNode(7); root->left->right->right = new TreeNode(4); TreeNode* p = root->left; TreeNode* q = root->left->right->right; Solution solution; TreeNode* lca = solution.lowestCommonAncestor(root, p, q); cout << lca->val << endl; return 0;}Complexity Analysis
Let N be the number of nodes in the binary tree and H be the height of the tree.
Time Complexity: O(N). Building the parent mapping using BFS may require visiting all N nodes. Traversing the ancestor chains of p and q requires at most O(H) additional time.
Space Complexity: O(N), where N is the number of nodes in the binary tree. The parent mapping may store every node, while nodesQueue and the ancestors set can also require additional linear space in the worst case.
Optimal Approach
The LCA can be found without storing complete root-to-node paths or explicit parent relationships.
For every recursive call, the following question is considered:
Does this subtree contain p, q, or their LCA?
Two variables, leftResult and rightResult, are used to store what is returned from the left and right subtree searches. These names make their purpose explicit: each variable represents useful information discovered within its respective subtree.
If the current node is p or q, that node is returned upward because one of the required targets has been found and may itself become the LCA.
If both leftResult and rightResult are non-null, one target has been found on each side. Therefore, the current node is the deepest point where the two branches meet.
If only one result is non-null, that result is propagated upward because both targets may lie in that branch, or one target may be an ancestor of the other.
Algorithm
If the current node is
null,nullis returned because the subtree contains neither target.If the current node is equal to
porq, the current node is returned because one target has been found and may itself be the Lowest Common Ancestor.The result of searching the left subtree is stored in
leftResult, while the result of searching the right subtree is stored inrightResult, so information found in both branches can be compared.If both
leftResultandrightResultare non-null, the current node is returned because the two target nodes have been found in different branches.If only
leftResultis non-null, it is propagated upward because the useful result lies in the left subtree.Otherwise,
rightResultis propagated upward because the useful result lies in the right subtree or neither target has been found.
Dry Run
Lowest Common Ancestor Optimal Approach Dry Run .png
Solution
#include <bits/stdc++.h>using namespace std;struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int value) { val = value; left = nullptr; right = nullptr; }};class Solution {public: // Returns p, q, or their LCA // when found inside the current subtree. TreeNode* lowestCommonAncestor( TreeNode* root, TreeNode* p, TreeNode* q ) { if (root == nullptr) { return nullptr; } // A target node may itself // be the Lowest Common Ancestor. if (root == p || root == q) { return root; } // leftResult and rightResult capture // useful nodes found in each subtree. TreeNode* leftResult = lowestCommonAncestor( root->left, p, q ); TreeNode* rightResult = lowestCommonAncestor( root->right, p, q ); // Non-null results from both sides mean // the targets lie in different branches. if ( leftResult != nullptr && rightResult != nullptr ) { return root; } // A single non-null result is propagated // upward until another target is found. if (leftResult != nullptr) { return leftResult; } return rightResult; }};int main() { TreeNode* root = new TreeNode(3); root->left = new TreeNode(5); root->right = new TreeNode(1); root->left->left = new TreeNode(6); root->left->right = new TreeNode(2); root->right->left = new TreeNode(0); root->right->right = new TreeNode(8); root->left->right->left = new TreeNode(7); root->left->right->right = new TreeNode(4); TreeNode* p = root->left; TreeNode* q = root->left->right->right; Solution solution; TreeNode* lca = solution.lowestCommonAncestor(root, p, q); cout << lca->val << endl; return 0;}Complexity Analysis
Let N be the number of nodes in the binary tree and H be the height of the tree.
Time Complexity: O(N), where N is the number of nodes in the binary tree. Each node is visited at most once during the recursive traversal.
Space Complexity: O(H), where H is the height of the binary tree, due to the recursion stack. This becomes O(N) for a skewed tree and O(log N) for a balanced tree.
Interview follow-up Questions
Yes. If one target node is an ancestor of the other, that node is the LCA. For example, if p = 5 and node 4 lies inside the subtree of 5, then the LCA of 5 and 4 is 5.
Be the first to add a comment.