Lowest Common Ancestor of a Binary Tree II

Updated on 16 June, 2025
Lowest Common Ancestor of a Binary Tree II header image

Problem Statement

In the context of a binary tree, the task is to identify and return the lowest common ancestor (LCA) of two given nodes, referred to as p and q. The LCA is defined as the lowest node in the tree from which both p and q are descendants. A descendant includes the node itself, meaning a node can be its own descendant. If one or both of the specified nodes p or q do not exist in the given binary tree, the function should return null. It is important to note that all node values within the tree are unique, which simplifies identification but requires careful verification of node existence.

Examples

Example 1

Input:

root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1

Output:

3

Explanation:

The LCA of nodes 5 and 1 is 3.

Example 2

Input:

root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4

Output:

5

Explanation:

The LCA of nodes 5 and 4 is 5. A node can be a descendant of itself according to the definition of LCA.

Example 3

Input:

root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 10

Output:

null

Explanation:

Node 10 does not exist in the tree, so return null.

Constraints

  • The number of nodes in the tree is in the range [1, 104].
  • -109 <= Node.val <= 109
  • All Node.val are unique.
  • p != q

Approach and Intuition

  1. Base Cases and Initial Checks:

    • If the tree is empty (root is null), directly return null as no ancestor can exist.
    • If either p or q matches the root, that root is the LCA, since a node can be an ancestor to itself.
  2. Recursive Traversal:

    • Use a recursive approach to search for nodes p or q in both the left and right subtrees.
    • A return of a non-null node from a subtree indicates the presence of either p or q.
    • If both left and right subtrees of a node return non-null values, that node is the LCA.
  3. One Side Returns Null:

    • If searching from a node results in one subtree returning null and the other returning a non-null value (either p or q), continue traversal up from the non-null side.
    • This step ensures that even if both nodes aren't immediate children of the current node, traversal continues up the tree to find the LCA.
  4. Handling Non-Existence:

    • Start at the root and verify existence of both nodes p and q using a separate validation function. This ensures correctness before proceeding with LCA logic.
    • If the validation fails (either p or q does not exist in the tree), return null.

This approach utilizes depth-first search principles, and by effectively checking both subtrees of each node, it efficiently narrows down the possible position of the LCA. The method relies on the fact that a node is identified as the LCA only if both nodes p and q are first encountered underneath this particular node, which is why checking both branches and looking up from the bottom (leaf nodes towards the root) is crucial.

Solutions

  • C++
  • Java
  • Python
cpp
class Solution {
public:
    TreeNode* findLCA(TreeNode* root, TreeNode* p, TreeNode* q) {
        bool found = false;

        function<TreeNode*(TreeNode*)> traverse = [&](TreeNode* current) {
            if (!current) return current;

            TreeNode* leftResult = traverse(current->left);
            TreeNode* rightResult = traverse(current->right);

            int matchCount = 0;
            if (current == p || current == q) matchCount++;
            if (leftResult != NULL) matchCount++;
            if (rightResult != NULL) matchCount++;
            if (matchCount == 2)
                found = true;

            if ((leftResult && rightResult) || (current == p) || (current == q)) return current;

            return leftResult ? leftResult : rightResult;
        };

        TreeNode* result = traverse(root);
        return found ? result : NULL;
    }
};

This solution addresses finding the Lowest Common Ancestor (LCA) of two nodes in a binary tree even when the nodes might not exist in the tree. The approach is implemented in C++ and involves a recursive traversal mechanism. Specifically, the method findLCA is designed to determine which node, if any, is the common ancestor of nodes p and q.

Here's the breakdown of the implemented logic:

  • A recursive lambda function, traverse, is declared within findLCA. This lambda is used for depth-first tree traversal.
  • During the traversal, the lambda checks each node to see if it is one of the targets (p or q) or if it received a target node from its left or right subtree.
  • It utilizes three pointers for each node: the current node itself, and the results from traversing the left and right children.
  • A local integer, matchCount, keeps track of how many of the nodes (current, left, or right) match either p or q.
  • If matchCount equals two at any node, this node is marked as a potential LCA and found is set to true. This marking preempts further match searches since the LCA is resolved.
  • The lambda returns:
    • the current node if it or both of its children are the target nodes,
    • the non-null child if only one child contains a target,
    • or null if neither the node nor its children are the targets.
  • The final result of the traverse function is evaluated. If found is true, the function returns the marked LCA; otherwise, it returns NULL, indicating no LCA was found or one/both nodes do not exist in the tree.

The function ensures efficiency by combining the traversal and identification steps into a single recursive loop, avoiding the need to traverse the tree multiple times. In practice, this approach effectively finds the LCA under the constraints that both nodes might be absent from the tree. It also includes robust handling for cases where no LCA exists due to missing nodes, providing a clear distinction by returning NULL.

java
class Solution {

    boolean foundBoth = false;

    public TreeNode findAncestor(TreeNode root, TreeNode p, TreeNode q) {
        TreeNode result = searchTree(root, p, q);
        return foundBoth ? result : null;
    }

    private TreeNode searchTree(TreeNode current, TreeNode p, TreeNode q) {
        if (current == null) return null;

        TreeNode leftSearch = searchTree(current.left, p, q);
        TreeNode rightSearch = searchTree(current.right, p, q);

        int matching = 0;
        if (current == p || current == q) matching++;
        if (leftSearch != null) matching++;
        if (rightSearch != null) matching++;
        if (matching == 2) foundBoth = true;

        if (current == p || current == q || (leftSearch != null && rightSearch != null)) return current;

        return leftSearch != null ? leftSearch : rightSearch;
    }
}

The code provided manipulates a binary tree to find the lowest common ancestor (LCA) of two nodes, referred to as p and q. It employs recursion to traverse the tree and identify the LCA only if both nodes are present in the tree. The process leverages a helper function searchTree alongside a boolean flag foundBoth to keep track of whether both nodes have been found during the traversal.

The workflow of the code is as follows:

  • The main function findAncestor initializes the search by calling searchTree and provides the root of the binary tree and the two nodes p and q.
  • The helper function searchTree recursively explores the tree. It performs a post-order traversal (explore left, explore right, process node):
    • It first recursively checks the left and right children of the current node.
    • It determines the number of matches among the current node, left child, and right child with either p or q.
    • If both nodes are identified at this stage (i.e., the sum of matches equals 2), the boolean flag foundBoth is set to true.
    • The function then decides whether the current node is the LCA based on conditions:
      • If either the current node is one of p or q, or both left and right recursive calls returned nodes (implying that p and q are found in different branches of this node), the current node is returned as the possible LCA.
    • If not conclusively determined, it propagates up the LCA found in either the left or right child, favoring non-null results.
  • Finally, findAncestor checks the foundBoth flag. If true, it returns the node identified as the LCA. If either node p or q wasn't found during the traversal, it returns null.

This technique ensures that the function only returns the LCA if both target nodes p and q are indeed present in the provided binary tree. This method is efficient in terms of space complexity primarily using recursive stack space, and it ensures a thorough search through both left and right subtrees, meeting the conditions for identifying the lowest common ancestor in a binary tree.

python
class Solution:
    def findLCA(self, root: "TreeNode", node1: "TreeNode", node2: "TreeNode") -> "TreeNode":
        self.is_complete = False

        def traverse(current):
            if not current:
                return current
            left_child = traverse(current.left)
            right_child = traverse(current.right)

            num_conditions = 0
            if current in (node1, node2):
                num_conditions += 1
            if left_child:
                num_conditions += 1
            if right_child:
                num_conditions += 1
            if num_conditions == 2:
                self.is_complete = True
    
            if (left_child and right_child) or current in (node1, node2):
                return current
                
            return left_child or right_child

        result = traverse(root)
        return result if self.is_complete else None

The provided Python code defines a method to find the Lowest Common Ancestor (LCA) of two nodes in a binary tree with an approach that guarantees both nodes exist in the tree.

The class Solution includes a method findLCA which takes three arguments: root, the root of the binary tree; node1, the first node to find the LCA for; and node2, the second node. The method initializes a boolean instance variable, is_complete, to track whether both nodes have been found during traversal.

The nested traverse function recursively explores the tree:

  • It returns None if it reaches a leaf node (end of a branch).
  • The function recursively calls itself for the left and right children of the current node.
  • It counts conditions where the current node matches either of the target nodes or if the left or right child returns a non-null result, indicating one of the target nodes was found in that subtree.
  • If exactly two conditions are met (meaning the current node is the lowest common ancestor), is_complete is set to True.
  • If a path contains either target node or both children have the target nodes, the current node is returned as the LCA.
  • Otherwise, the function returns either the left or right child's result, whichever is non-null.

The main function findLCA calls the traverse function starting from the root and assigns its output to result. The method concludes by checking is_complete; if true, it returns the LCA (result), otherwise, it returns None, indicating one or both target nodes are not present in the tree.

Key Details in This Implementation:

  • The binary tree doesn't need to be balanced.
  • Node existence is validated, ensuring the solution only returns an LCA if both nodes are present in the tree. This adds robustness to the function.
  • The entire tree is potentially traversed in the worst case, making the time complexity O(n), where n is the number of nodes in the tree.

This code is ideally suited for scenarios where node existence in the tree needs verification before determining the LCA.

Comments

No comments yet.