Introduction

Daniel (🧔🏽‍♂️) nodded his head slowly as he added cream to his freshly poured coffee. He had worked hard to learn his Leetcode and he was feeling ready. He took a sip and walked over to his laptop and sat down. Moments later the interview began.

👩 “Good afternoon Daniel! Thanks for taking the time to interview with me today. Let’s begin with some brief introductions. My name is Jordanna! I’m a senior engineer on the user performance improvement team and I will be conducting your coding interview today.”

🧔🏽‍♂️ “Thank you for this opportunity, I’m looking forward to it. I graduated with a B.Sc. in civil engineering from UTSA two years ago and I’ve spent the last year and a half working DuBank as a junior software engineer.”

👩 “It’s a pleasure to meet you Daniel. Let’s dive into this.” Jordanna (👩 ) copied the problem into the shared code editor and Daniel reads the problem statement as he takes a slow sip of his refreshing medium roast.


Problem Statement

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:

                3

    5                       1

6       2               0       8

    7       4

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:

                3

    5                       1

6       2               0       8

    7       4

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

🧔🏽‍♂️ “We are given a binary tree and we are asked to find the lowest common ancestor. If I understand correctly, this is not a binary search tree, am I correct?”

👩 “Yes, as we can see in the examples, the nodes are not sorted.”

🧔🏽‍♂️ “What is the range on the number of nodes that we can expect to see?”

👩 “We can expect anywhere from 2 to 10,000 nodes.”

🧔🏽‍♂️ ”Are all node values unique?”

👩 “Yes, all node values are unique.”

🧔🏽‍♂️ “Can we ever have a case where p and q are equal?”

👩 “No, p will never be equal to q.”

🧔🏽‍♂️ “Ok, thanks! That helps. If we look at the root node of the tree, there are three different case that we need to account for. One: both nodes p and q could be in there left subtree, Two: one node could be in the left subtree and the other could be in the right subtree. Three: both nodes could be in the right subtree. Would you say that I have accurately described the different scenarios?”

👩 “I like where you are going with this, Daniel! It’s a good start. Are there any other cases that you can think of?”

🧔🏽‍♂️ “Let me scan the problem statement again… Ah, yes! The LCA could be one of the nodes. We could have a case where p is a direct ancestor of q or vice versa. I believe this expands upon cases one and three. Would you say that my articulation of the problem is correct? ”

👩 “Yes, it sounds like you’re ready to start coming up with a solution. How would you write this up?””

🧔🏽‍♂️ “Thanks Jordanna, I’d love to! Since this is a tree traversal problem, it can be solved using recursion. Since I’ll be using Python, I’ll need to be mindful of the recursion limit. By default, the recursion limit in Python is typically set to 1,000. I’ll begin by setting it to 20,000 at the beginning of my solution and I’ll change it back at the end. Are you ok with this approach?”

👩 “Yes, so far so good. How would you write the recursive algorithm?”

🧔🏽‍♂️ “At every node, we will recursively traverse down the left subtree and then down the right. When we reach the leftmost leaf node, the next left subtree recursive call will return None and the next right subtree recursive call will return None. We can use left and right variables to keep track of these returned values.”

👩 Jordanna nodded her head in agreement.

🧔🏽‍♂️ “If the root value equals that of p or q, we return the root address up the to the next stack frame. If left and right are not None, we have found our LCA and return root. At this point in the else if, we only have two options remaining. If left is not None, we return left, otherwise we return right.” Daniel paused for a moment, and continued. “If p or q was found in the right subtree, right will equal the node’s memory address, otherwise, we will return None. As this algorithm runs it course, the LCA node address will be returned all of the way up to the top stack frame until it is return to the original caller.”


Recursive Solution (p & q exist)

🧔🏽‍♂️ Daniel wrote the following code and made sure to add comments as we went along. He then ran through an example input and validated the results.

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
        """
            Time: O(N)
            Space: O(N)
        """
        if not root:
            return None
        
        # Recursively search through the left and then the right branches
        left  = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)

        # return the answer ~ last call
        if root.val == p.val or root.val == q.val:
            # Return the node when we find it
            return root
        elif left and right:
            # If left and right are not None, we have found
            # the LCA. Return it up to the next stack frame.
            return root
        elif left:
            # Since p or q was found in the left subtree,
            # return it up the call stack.
            return left
        else:
            # if p or q was found in the right subtree,
            # it will be returned up, if neither were 
            # found, we will return None. This means that
            # we have not found either or our target nodes
            # yet.
            return right

Recursive solution (p or q may not exist)

👩 “Daniel this looking good! As a follow-up. Let’s say that we no longer can guarantee that both p and q exist in the binary tree. How would this change your solution?”

🧔🏽‍♂️ “Ok, let’s see…” Daniel took a moment and reviewed his solution and continued. “If we don’t have two target nodes in the binary tree, my current solution will return the memory address of the one node we did find. I can see how this could cause problems. If a valid LCA does not exist in the tree, I would expect a None response. I believe we will need to keep track of the number nodes that have been found. Am I on the right track?”

👩 “Yes, you’re on the right track. Please continue.”

🧔🏽‍♂️ “I’m thinking we can traverse through the binary tree in the same way we did in the original solution. Only this time we’ll use a traverse(...,ans:TreeNode=None) function with a default parameter to keep track of the LCA node. This function will return a (flag, ans) tuple. We can use left and right variables to keep track of the left and right subtree search result flags. If we find p or q at a node, we will return a tuple with the flag and None (1, None). After we traverse the left and right subtrees for each node, we can set flag = 0 for that node and then check if we have found the LCA. Jordanna, do you have any comments or concerns with where I’m going with this?”

👩 “So far so good Daniel.”

🧔🏽‍♂️ “Ok, if the current node is equal to either p or q, we will set flag = 1. After that, we can sum up the flags and see if we have found the LCA yet. If (left = 1) or (right = 1) and flag = 1, we will set ans = node. If (left = 1) and (right = 1) and flag = 0, we will set ans = node. Once we have found the LCA, left + flag + right will equal 1 and ans will equal the LCA node for the remainder of the recursive calls. The ans will be returned through the call stack up to the top and returned to the original caller. If we only ever find 1 target node, we will never set the ans to a node memory address and will eventually return None as our final answer."

👩 “We still have some time left here, can you please code it up for me?”

🧔🏽‍♂️ “For sure!" Daniel responded cheerfully and hammered out the solution below."

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

from typing import Tuple

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        def traverse(node: TreeNode, p: TreeNode, q:TreeNode, ans: TreeNode = None) -> Tuple[Any]:
            """
                Time: O(N)
                Space: O(N)
            """
            if not node:
                return 0, ans
            
            # Post order traversal through binary tree
            left, ans = traverse(node.left, p, q, ans = ans)
            right, ans = traverse(node.right, p, q, ans = ans)

            flag = 0

            # Set flag = 1 if p or q found
            if node.val == p.val or node.val == q.val:
                flag = 1
            
            # The sum of the flags will be greater
            # than 1 if p & q have been found
            if left + flag + right == 2:
                ans = node
            
            # If p or q found in a subtree or 
            # the current node, return 1
            if left or flag or right:
                return 1, ans
            else:
                return 0, ans
            
        _, ans = traverse(root, p, q)

        return ans

👩 “Can you run through an example for me?”

🧔🏽‍♂️ “Are you ok with me pointing my camera towards my sheet of paper here so that I can draw a tree and run through an example?”

👩 “Yes, for sure!”

🧔🏽‍♂️ Daniel worked through an example and validated his solution.

👩 After reviewing Daniel’s solution, Jordanna closed the meeting: “Thanks for taking the time interview with me today!”

🧔🏽‍♂️ “Thanks Jordanna, have a great afternoon!”

👩 “You’re welcome Daniel! You too!”

Leetcode 236 Lowest Common Ancestor of a Binary Tree