Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 34

Finding the Lowest Common Ancestor in a BST

Recommended resources

Hello! Welcome back to our series on Binary Search Trees.

In our last lesson, we saw how the sorted nature of an in-order traversal allows for an elegant solution to finding the k-th smallest element. Today, we'll continue this theme of leveraging the unique properties of a BST to solve another classic interview problem: finding the Lowest Common Ancestor (LCA) of two nodes.

The goal of this lesson is to understand the efficient algorithm for finding the LCA in a BST and be able to implement it. This is a very common question because it tests your understanding of the core BST invariant and your ability to craft a solution that is significantly better than what's possible in a general binary tree.

What is a Lowest Common Ancestor?

The Lowest Common Ancestor (LCA) between two nodes, p and q, in a tree is defined as the lowest (i.e., deepest) node that has both p and q as descendants. A node can be a descendant of itself.

Let's look at a visual example.

Least Common Ancestor (LCA) in Binary Search Tree Examples
CAPTION: In the left tree, the LCA of nodes 2 and 6 is 4. In the right tree, the LCA of nodes 2 and 12 is 8, because 8 is the deepest node that is an ancestor to both.

For a general binary tree, a common approach is to find the path from the root to p and the path from the root to q, and then find the last common node in both paths. This works, but it's inefficient, requiring time and space in the worst case. For a BST, we can do much better.


The Key Insight: Leveraging the BST Property

The defining property of a BST is that for any given node, all values in its left subtree are smaller, and all values in its right subtree are larger. We can use this to our advantage.

Imagine you are at the root of the tree, looking for the LCA of p and q. There are three possibilities:

  1. Both p and q have values smaller than the current node's value. This means the LCA must be in the left subtree.
  2. Both p and q have values larger than the current node's value. This means the LCA must be in the right subtree.
  3. The values of p and q split at the current node: one is smaller (or equal) and one is larger (or equal). This means the current node is the "fork in the road." It's the first ancestor from the top that has p and q in different subtrees. Therefore, the current node is the LCA. This case also handles the situation where one of the target nodes is the ancestor of the other.

This logic allows us to discard half of the tree at each step, similar to binary search.

To see this logic in action, let's watch a short video that explains it visually.

Lowest Common Ancestor of a Binary Search Tree - Leetcode 235 - Python

This video from NeetCode explains the core logic behind finding the LCA in a BST by identifying the 'split point'. Pay close attention to the three cases he describes.

Watch from 00:46 to 04:44. This segment defines the LCA, explains how the split point works, and covers the edge case where one node is an ancestor of the other.


The Algorithm: Iterative Approach

As the video demonstrates, this logic translates into a highly efficient iterative algorithm. This approach is generally preferred in interviews because it uses constant extra space.

Here is the algorithm:

  1. Start with a pointer, current, at the root of the tree.
  2. Loop as long as current is not null.
  3. If both p.val and q.val are greater than current.val, move to the right child: current = current.right.
  4. If both p.val and q.val are less than current.val, move to the left child: current = current.left.
  5. If neither of the above is true, it means we've found the split point (or one of the nodes is the LCA). The current node is the LCA, so we return it.

Let's see the code implementation of this.

Lowest Common Ancestor of a Binary Search Tree - Leetcode 235 - Python

Now, let's see how this logic translates into a clean, iterative Python solution. This is the implementation you'd want to aim for in an interview.

Watch the code walkthrough from 05:10 to 06:32. Notice how the simple while loop directly implements the three cases we just discussed.

Here is the Python code for your reference. It's a pattern worth committing to memory.

# 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':
        current = root
        
        while current:
            # If both p and q are greater than parent
            if p.val > current.val and q.val > current.val:
                current = current.right
            # If both p and q are smaller than parent
            elif p.val < current.val and q.val < current.val:
                current = current.left
            # We have found the split point, i.e. the LCA
            else:
                return current
Test your understanding!

Consider the BST from the image above (the one on the right, rooted at 8). If you are looking for the LCA of nodes p=10 and q=14, trace the path of the current pointer using the iterative algorithm. What nodes does it visit?

Show answer
  1. current starts at root (node 8).
  2. p.val (10) > current.val (8) and q.val (14) > current.val (8). Both are larger. So, current moves to current.right (node 12).
  3. Now current is at node 12.
  4. p.val (10) < current.val (12) and q.val (14) > current.val (12). This is a split! The else block is executed.
  5. The algorithm returns current, which is node 12.

The visited nodes are 8 and 12. The LCA is 12.


Alternative: Recursive Approach

The same logic can be implemented recursively. The code is often more concise, but it comes at the cost of using extra space on the call stack.

Lowest Common Ancestor in a Binary Search Tree

For completeness, let's look at the recursive solution. The logic is identical but expressed through recursive calls. The article 'Lowest Common Ancestor in a Binary Search Tree' from GeeksforGeeks provides a clear implementation.

Please read the section '[Better Approach] Using BST Properties (Recursive Approach)'. Focus on the Python code provided to see the recursive structure and how it mirrors the iterative logic.

Here's the recursive Python code from the article:

def LCA(root, n1, n2):
    if root is None:
        return None

    # If both n1 and n2 are smaller than root, LCA lies in left
    if root.data > n1.data and root.data > n2.data:
        return LCA(root.left, n1, n2)

    # If both n1 and n2 are greater than root, LCA lies in right
    if root.data < n1.data and root.data < n2.data:
        return LCA(root.right, n1, n2)

    # If we reach here, then root is the LCA
    return root

Complexity Analysis

Let's quickly summarize the performance, which is the main reason this approach is so powerful.

  • Time Complexity: , where is the height of the tree. At each level, we make one decision and eliminate the rest of the tree at that level. For a balanced BST, this becomes . In the worst case (a skewed tree), it's .
  • Space Complexity:
    • Iterative: — We only use a few pointers, regardless of the tree's size. This is optimal.
    • Recursive: — Due to the recursion call stack. For a balanced BST, this is ; for a skewed tree, .

This is a significant improvement over the time and space complexity of the naive approach for a general binary tree.


Conclusion

In this lesson, we tackled another fundamental BST problem, perfect for technical interviews. You've learned how to exploit the BST ordering property to find the Lowest Common Ancestor efficiently.

Key Takeaways:

  • The LCA in a BST is the "split point" node, where the paths to the two target nodes diverge.
  • The search for the LCA can be done in time by traversing from the root and deciding at each node whether to go left, go right, or stop.
  • The iterative solution is generally preferred due to its optimal space complexity.
  • Be prepared to explain why this approach is superior to methods used for general binary trees.

In our next and final lesson on BSTs, we'll address another common problem: constructing a height-balanced BST from a sorted array. This will tie together concepts of tree balance and BST properties. See you then