Skip to content

Finding Lowest Common Ancestor in Binary Tree Python Guide

Updated: at 05:12 AM

The lowest common ancestor (LCA) of two nodes in a binary tree is the lowest (deepest) node that has both nodes as descendants. Finding the LCA of two nodes has many practical applications in computer science, including optimizing network routing and analyzing hierarchical data structures. This comprehensive guide will explain the concept of the lowest common ancestor in binary trees and provide Python code examples to implement solutions for finding the LCA.

Table of Contents

Open Table of Contents

Overview of Binary Trees

A binary tree is a tree data structure where each node has at most two child nodes referred to as the left child and right child. The top-most node in the tree is called the root. In Python, a binary tree can be represented as a class with attributes for the data value, left child node, and right child node:

class Node:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

We can construct a sample binary tree structure:

           1
         /   \
        2     3
      /  \
     4    5

To traverse and search binary trees in Python, we can use recursive functions to traverse left and right child nodes. Common types of tree traversals include preorder, inorder, and postorder traversal.

Lowest Common Ancestor Concept

The lowest common ancestor (LCA) of two nodes n1 and n2 is defined as the lowest (deepest) node that has both n1 and n2 as descendants. Some key properties of the LCA include:

To find the LCA of two nodes in a binary tree, we need to leverage tree traversal methods to search possible ancestor nodes while keeping track of the deepest or lowest matching ancestor.

Naive Approach in Python

A simple way to find the LCA is to traverse the binary tree starting from the root node. Check if either of the two given nodes n1 and n2 matches the current node. If there is a match, return the current node as the LCA.

Here is an example Python implementation:

# Function to find LCA of n1 and n2
def findLCA(root, n1, n2):
    # Base case
    if root is None:
        return None

    # Check if current node is n1 or n2
    if root.val == n1 or root.val == n2:
        return root

    # Recursively search left and right subtrees
    left = findLCA(root.left, n1, n2)
    right = findLCA(root.right, n1, n2)

    # If left and right both not null, current node is LCA
    if left and right:
        return root

    # Otherwise, return left or right subtree result
    return left if left else right

This naive approach works but is not optimal, with a worst-case time complexity of O(N) to search the full tree where N is the number of nodes. We can optimize this further.

Efficient Approach with Tree Traversal

A more efficient solution is to traverse the binary tree using tree traversal methods like inorder, preorder or postorder traversal. While traversing, if we come across the two given nodes, the current node must be the LCA.

We can stop traversing further down a subtree if both nodes are found as descendants. This prunes off portions of the tree from our search, improving efficiency.

Here is an example preorder traversal based Python implementation:

# Function to find LCA using preorder traversal
def findLCA(root, n1, n2):

  # Base case
  if root is None:
    return None

  # If either n1 or n2 matches current node, return the node
  if root.val == n1 or root.val == n2:
    return root

  # Recursively traverse left and right subtrees
  left_lca = findLCA(root.left, n1, n2)
  right_lca = findLCA(root.right, n1, n2)

  # If both recurse calls return a node, current node is LCA
  if left_lca and right_lca:
    return root

  # Otherwise, return the non-null left or right LCA node
  return left_lca if left_lca else right_lca

The time complexity is reduced to O(H) where H is the height of the binary tree. For a balanced tree, this is O(logN) which is much more efficient.

We traverse down until we find one of the nodes, then continue traversing to find the second node. The first match of the second node will be the lowest common ancestor.

Handling When LCA is One of the Nodes

In the above solutions, we assumed the LCA node itself cannot be equal to either n1 or n2. We need some small modifications to handle when the LCA is one of the nodes itself.

We can track if we have found n1 and n2 while traversing the tree. If both are found, and the current node is one of the two nodes, this node must be the LCA node.

# Track if n1 and n2 are found
n1_found = False
n2_found = False

# Modified preorder traversal based function
def findLCA(root, n1, n2):

  nonlocal n1_found, n2_found

  if root is None:
    return None

  # If current node is n1 or n2, mark as found
  if root.val == n1:
    n1_found = True
    return root if n2_found else None
  elif root.val == n2:
    n2_found = True
    return root if n1_found else None

  left_lca = findLCA(root.left, n1, n2)
  right_lca = findLCA(root.right, n1, n2)

  # If both nodes found, return current node
  if n1_found and n2_found or left_lca and right_lca:
    return root

  # Else return non-null left or right LCA
  return left_lca if left_lca else right_lca

This correctly handles when the LCA node is one of n1 or n2 itself.

LCA When Nodes are Not Present

To handle when one or both the nodes are not present in the tree, we can return None in those cases:

def findLCA(root, n1, n2):

  if root is None:
    return None

  # If n1 or n2 not found yet, recurse
  if not n1_found:
    left_lca = findLCA(root.left, n1, n2)
  if not n2_found:
    right_lca = findLCA(root.right, n1, n2)

  # If both nodes not present, return None
  if not n1_found and not n2_found:
    return None

  # Else return current node as LCA
  return root

This checks if n1 and n2 are found before recursing into subtrees. If any node is not found after full traversal, we can conclude that node is not present and return None.

Finding Distance from LCA

Once we find the LCA node, we can also find the distance of n1 and n2 from the LCA.

We traverse back up from n1 and n2 tracking the depth/distance. The sum of depths of n1 and n2 from LCA will be the distance between n1 and n2.

Here is an example implementation:

# Function to find distance of n1 and n2 from the LCA node
def findDistance(lca, n1, n2):

  d1 = getDepth(lca, n1)
  d2 = getDepth(lca, n2)

  return d1 + d2

# Get depth of node from LCA
def getDepth(lca, node):

  depth = 0
  while node != lca:
    depth += 1
    node = node.parent

  return depth

This allows us to calculate the distance between two nodes after finding the LCA node.

Summary

In this guide, we looked at different methods to find the lowest common ancestor of two given nodes in a binary tree using Python:

The lowest common ancestor is a fundamental concept for navigating and analyzing hierarchical tree structures with many useful applications in graph theory, networks, and bioinformatics. This guide provided various Python implementations to find the LCA in different scenarios.