Taro Logo

Lowest Common Ancestor of a Binary Tree

Medium
Meta logo
Meta
115 views
Topics:
TreesRecursion

Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.

According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

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, since a node can be a descendant of itself according to the LCA definition.

Example 3:

Input: root = [1,2], p = 1, q = 2
Output: 1

Constraints:

  • The number of nodes in the tree is in the range [2, 105].
  • -109 <= Node.val <= 109
  • All Node.val are unique.
  • p != q
  • p and q will exist in the tree.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. Can the binary tree be empty, or can p or q be null?
  2. Are p and q guaranteed to exist in the tree?
  3. Are the node values unique within the tree, or could there be duplicates?
  4. Can a node be a descendant of itself, as the problem definition implies?
  5. If p or q is the root, is the root still considered the lowest common ancestor?

Brute Force Solution

Approach

The brute force method for finding the lowest common ancestor in a family tree involves checking every possible ancestor. We explore all ancestor combinations for the two given individuals until we find the deepest one they share.

Here's how the algorithm would work step-by-step:

  1. First, find all the ancestors of the first person.
  2. Then, find all the ancestors of the second person.
  3. Next, compare the two lists of ancestors to find the ones they have in common.
  4. Finally, from all the common ancestors, choose the one that is the furthest down the family tree (the deepest). This is the lowest common ancestor.

Code Implementation

def lowest_common_ancestor_brute_force(root, node_one, node_two):
    def find_ancestors(node, target, ancestors_list):
        if not node:
            return False
        
        if node == target:
            ancestors_list.append(node)
            return True

        if find_ancestors(node.left, target, ancestors_list) or \
           find_ancestors(node.right, target, ancestors_list):
            ancestors_list.append(node)
            return True

        return False

    ancestors_node_one = []
    ancestors_node_two = []

    find_ancestors(root, node_one, ancestors_node_one)
    find_ancestors(root, node_two, ancestors_node_two)

    # Need to reverse to find from root to node
    ancestors_node_one.reverse()
    ancestors_node_two.reverse()

    lowest_common_ancestor = None

    # Find the common ancestors
    for i in range(min(len(ancestors_node_one), len(ancestors_node_two))):
        if ancestors_node_one[i] == ancestors_node_two[i]:
            lowest_common_ancestor = ancestors_node_one[i]
        else:
            break

    # Return the last common ancestor
    return lowest_common_ancestor

Big(O) Analysis

Time Complexity
O(n²)Finding all ancestors for each node in a binary tree of n nodes can take O(n) time in the worst case (e.g., skewed tree). Since we need to do this for both nodes, that's already O(n). Then, comparing the two lists of ancestors, each of which can be of size O(n), requires comparing each element in one list to each element in the other list, leading to O(n*n) operations. The final step of finding the deepest common ancestor among the common ancestors takes at most O(n) time. Thus the dominant factor is the comparison of the ancestor lists, leading to O(n²).
Space Complexity
O(N)The space complexity is determined by the storage of ancestors for both input nodes. In the worst-case scenario, where the binary tree is skewed, the list of ancestors for each node could potentially contain all the nodes from the root to that node. This means we could store two lists, each potentially of size N, where N is the number of nodes in the tree. Therefore, the auxiliary space required to store these ancestor lists grows linearly with the number of nodes in the tree, resulting in a space complexity of O(N).

Optimal Solution

Approach

The most efficient way to find the lowest common ancestor is to use a method that explores the tree only as needed. We'll use recursion to search for the target nodes and simultaneously identify the common ancestor. The key idea is that if a node has both target nodes in its subtrees, it is the lowest common ancestor.

Here's how the algorithm would work step-by-step:

  1. Start at the root of the tree.
  2. For each node, check if it is one of the target nodes we're looking for. If it is, we remember that we've found one of the targets.
  3. Recursively check the left side of the node to see if it contains either of the target nodes.
  4. Recursively check the right side of the node to see if it contains either of the target nodes.
  5. If both the left and right sides contain one of the target nodes, then the current node is the lowest common ancestor. We have found our answer!
  6. If only one side contains target nodes, pass that side up the chain, meaning the ancestor lies on that side.
  7. If neither side has target nodes, pass up the signal that no target node was found in this area.

Code Implementation

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

def lowestCommonAncestor(root, first_node, second_node):
    if not root:
        return None

    # If the current node is one of the target nodes.
    if root == first_node or root == second_node:
        return root

    left_subtree_result = lowestCommonAncestor(root.left, first_node, second_node)

    right_subtree_result = lowestCommonAncestor(root.right, first_node, second_node)

    #If both subtrees have a result, this node is the LCA
    if left_subtree_result and right_subtree_result:
        return root

    #If only one subtree has a result, return that result
    return left_subtree_result if left_subtree_result else right_subtree_result

Big(O) Analysis

Time Complexity
O(n)The algorithm traverses the binary tree in a depth-first manner. In the worst-case scenario, the algorithm visits every node in the tree to find the lowest common ancestor. The recursion explores each node to check if it or its descendants contain the target nodes, effectively visiting each node once. Therefore, the time complexity is directly proportional to the number of nodes, n, in the binary tree.
Space Complexity
O(H)The space complexity is primarily determined by the recursion depth, where H is the height of the binary tree. In the worst-case scenario, such as a skewed tree, the recursion depth can be equal to N (the number of nodes), as each recursive call adds a new frame to the call stack until it reaches a leaf node. However, in a balanced tree, the height is logarithmic, resulting in a space complexity of O(log N). Therefore, the space complexity is O(H) reflecting the maximum depth of the recursion stack.

Edge Cases

CaseHow to Handle
Root is nullReturn null immediately, as an empty tree has no LCA.
p or q is nullReturn null if either p or q is null, as LCA is not defined.
Tree has only one node (root)Return the root if root is either p or q; otherwise return null.
p and q are the same nodeReturn p (or q) since a node is an ancestor of itself.
p is an ancestor of qReturn p since it is the lowest ancestor containing both.
q is an ancestor of pReturn q since it is the lowest ancestor containing both.
p or q is not in the treeThe standard recursive solution will still function correctly, ultimately returning null if only one or neither node is found by the search.
Skewed Tree (left or right)The solution should still work correctly, though the recursive calls might take longer to complete in a highly skewed tree.