Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1644. Lowest Common Ancestor of a Binary Tree II - Leetcode Solution

Problem Description

The Leetcode problem "Lowest Common Ancestor of a Binary Tree II" asks you to find the lowest common ancestor (LCA) of two given nodes, p and q, in a binary tree rooted at root. However, there is an important twist: either or both of the nodes p and q may not exist in the tree. If either p or q does not exist in the tree, you should return null (or None in Python).

  • Each node in the binary tree has a unique value.
  • There is no guarantee that both p and q exist in the tree.
  • The LCA of two nodes is defined as the lowest node in the tree that has both p and q as descendants (a node can be a descendant of itself).
  • If either p or q does not exist, return null.

Thought Process

The classic LCA problem assumes both target nodes exist in the tree. In that case, we can use a simple recursive approach: if the current node matches either p or q, we return it; otherwise, we search both left and right subtrees for the targets. If both subtrees return non-null, the current node is the LCA.

However, since p or q may not exist, we need to be careful. We can't just return the node where both sides meet; we must also make sure that both p and q are actually present in the tree. A brute-force way would be to search the tree twice to check for existence, then do the normal LCA search. But this is inefficient.

Instead, we can enhance our recursive function to not only look for the LCA, but also tell us whether p and q were found in the subtree. This way, we can combine existence checking and LCA finding in a single traversal.

Solution Approach

We will use a recursive helper function that, for each node, returns three things:

  • The LCA node if found (otherwise null).
  • A boolean indicating whether p was found in this subtree.
  • A boolean indicating whether q was found in this subtree.
  1. Base Case: If the current node is null, return (null, false, false).
  2. Recursive Case: Recurse on the left and right children.
  3. Check Current Node: See if the current node is p or q.
  4. Aggregate Results: If the current node is the LCA (i.e., one target is in the left subtree and the other is in the right, or the current node is one of the targets and the other is in a subtree), record it.
  5. Return Upwards: Pass up the LCA (if found), and booleans indicating if p or q was found in this subtree.
  6. Final Check: After the traversal, if both p and q were found, return the LCA; otherwise, return null.

This approach ensures we only traverse the tree once and handle all cases efficiently.

Example Walkthrough

Suppose we have the following tree:

        3
       / \
      5   1
     / \ / \
    6  2 0  8
      / \
     7   4
  

Let p = 5 and q = 4. Both exist in the tree.

  • We search left from root (3), reaching 5 (which is p).
  • From 5, we search its left (6, which is not q) and right (2).
  • From 2, we search left (7) and right (4, which is q).
  • We bubble up: 2's right subtree found q.
  • At 5, we now know p (itself) and q (from its right subtree) are both found. So, 5 is the LCA.
  • At the end, since both p and q were found, we return 5.

If q = 42 (which doesn't exist), our function will traverse the tree, find p but not q, so it returns null.

Time and Space Complexity

Brute-force approach: Search for p and q separately (O(N) each), then find LCA (O(N)). Total: O(N) time, but with multiple traversals.

Optimized approach (ours): Single tree traversal, each node visited once. So, O(N) time, where N is the number of nodes.

Space complexity: O(H), where H is the height of the tree, for the recursion stack. For a balanced tree, this is O(log N); for a skewed tree, O(N).

Summary

We solved the "Lowest Common Ancestor of a Binary Tree II" problem by augmenting the standard LCA algorithm to also check for the existence of both target nodes in a single traversal. By returning flags up the recursive call stack, we efficiently determine if both targets exist and, if so, return their LCA. This approach is elegant because it combines existence checking and LCA finding into one pass, ensuring optimal performance.

Code Implementation

class Solution:
    def lowestCommonAncestor(self, root, p, q):
        def helper(node):
            if not node:
                return (None, False, False)
            left_lca, left_p, left_q = helper(node.left)
            right_lca, right_p, right_q = helper(node.right)
            
            found_p = left_p or right_p or node == p
            found_q = left_q or right_q or node == q
            
            # If node is LCA
            if node == p or node == q:
                return (node, found_p, found_q)
            if left_lca and right_lca:
                return (node, found_p, found_q)
            if left_lca:
                return (left_lca, found_p, found_q)
            if right_lca:
                return (right_lca, found_p, found_q)
            return (None, found_p, found_q)
        
        lca, found_p, found_q = helper(root)
        return lca if found_p and found_q else None
      
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    struct Result {
        TreeNode* lca;
        bool found_p;
        bool found_q;
    };
    
    Result helper(TreeNode* node, TreeNode* p, TreeNode* q) {
        if (!node) return {nullptr, false, false};
        Result left = helper(node->left, p, q);
        Result right = helper(node->right, p, q);
        
        bool found_p = left.found_p || right.found_p || node == p;
        bool found_q = left.found_q || right.found_q || node == q;
        
        if (node == p || node == q) {
            return {node, found_p, found_q};
        }
        if (left.lca && right.lca) {
            return {node, found_p, found_q};
        }
        if (left.lca) {
            return {left.lca, found_p, found_q};
        }
        if (right.lca) {
            return {right.lca, found_p, found_q};
        }
        return {nullptr, found_p, found_q};
    }
    
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        Result res = helper(root, p, q);
        return (res.found_p && res.found_q) ? res.lca : nullptr;
    }
};
      
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    private static class Result {
        TreeNode lca;
        boolean foundP, foundQ;
        Result(TreeNode lca, boolean foundP, boolean foundQ) {
            this.lca = lca;
            this.foundP = foundP;
            this.foundQ = foundQ;
        }
    }
    
    private Result helper(TreeNode node, TreeNode p, TreeNode q) {
        if (node == null) return new Result(null, false, false);
        Result left = helper(node.left, p, q);
        Result right = helper(node.right, p, q);
        
        boolean foundP = left.foundP || right.foundP || node == p;
        boolean foundQ = left.foundQ || right.foundQ || node == q;
        
        if (node == p || node == q) {
            return new Result(node, foundP, foundQ);
        }
        if (left.lca != null && right.lca != null) {
            return new Result(node, foundP, foundQ);
        }
        if (left.lca != null) {
            return new Result(left.lca, foundP, foundQ);
        }
        if (right.lca != null) {
            return new Result(right.lca, foundP, foundQ);
        }
        return new Result(null, foundP, foundQ);
    }
    
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        Result res = helper(root, p, q);
        return (res.foundP && res.foundQ) ? res.lca : null;
    }
}
      
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    function helper(node) {
        if (!node) return {lca: null, foundP: false, foundQ: false};
        let left = helper(node.left);
        let right = helper(node.right);

        let foundP = left.foundP || right.foundP || node === p;
        let foundQ = left.foundQ || right.foundQ || node === q;

        if (node === p || node === q) {
            return {lca: node, foundP, foundQ};
        }
        if (left.lca && right.lca) {
            return {lca: node, foundP, foundQ};
        }
        if (left.lca) {
            return {lca: left.lca, foundP, foundQ};
        }
        if (right.lca) {
            return {lca: right.lca, foundP, foundQ};
        }
        return {lca: null, foundP, foundQ};
    }
    let result = helper(root);
    return (result.foundP && result.foundQ) ? result.lca : null;
};