
Problem Statement
In the given task, we are to determine the lowest common ancestor (LCA) of two specific nodes within a binary tree. The LCA between two nodes p
and q
is the deepest (or lowest) node that acts as an ancestral node to both p
and q
, inclusive of cases where a node can be its own ancestor. This task employs the classical understanding of ancestry within a tree structure, where the objective is to locate a node that directly connects the chosen nodes either through direct lineage or through common descent.
Examples
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
andq
will exist in the tree.
Approach and Intuition
The Lowest Common Ancestor (LCA) problem is a well-established problem in computer science, particularly within data structures concerning trees and graphs. To decode the LCA, one can leverage several intuitive observations from example evaluations:
Example 1
- Input:
root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
- Output:
3
- Explanation: The node values
5
and1
are descended from node3
. Hence, node3
is their lowest common ancestor.
Example 2
- Input:
root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
- Output:
5
- Explanation: Here, node
4
directly descends from node5
, and since a node can be its own ancestor, the LCA of nodes5
and4
is node5
.
Example 3
- Input:
root = [1,2], p = 1, q = 2
- Output:
1
- Explanation: The node
2
is a direct descendant of node1
. Given that a node can be a descendant of itself, node1
is the LCA of nodes1
and2
.
From these observations, the approach to finding the LCA can be described as follows:
- Start from the root and traverse the tree.
- If you encounter either
p
orq
during the traversal, that node itself becomes a potential LCA candidate. - If traversal leads to locating both
p
andq
on different subtrees of the current node, then the current node is the LCA. - If both nodes lie in the same subtree, move the search to that subtree and continue.
The logic primarily revolves around efficient tree traversal and keeping track of the paths or lineage of the nodes in question. Depth-first search (DFS) is particularly effective here, allowing the recursive exploration of all potential paths within the tree, while backtracking ensures that once a node has been evaluated, its ancestors are considered for LCA candidacy. This process continues until the conditions for determining the LCA, as illustrated through the examples, are conclusively met.
Solutions
- Java
- Python
class Solution {
// Constants to represent the state of node traversal
private static int PENDING_BOTH_CHILDREN = 2;
private static int DONE_LEFT_CHILD = 1;
private static int COMPLETED_TRAVERSAL = 0;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Stack<Pair<TreeNode, Integer>> traversalStack = new Stack<>();
// Begin with the root node, having both children pending
traversalStack.push(new Pair<TreeNode, Integer>(root, PENDING_BOTH_CHILDREN));
// Flag to identify whether one of the target nodes has been found
boolean isOneNodeFound = false;
// Variable to hold the lowest common ancestor
TreeNode ancestor = null;
// Node to help with child node processing
TreeNode nextNode = null;
while (!traversalStack.isEmpty()) {
Pair<TreeNode, Integer> nodePair = traversalStack.peek();
TreeNode currentNode = nodePair.getKey();
int state = nodePair.getValue();
if (state != COMPLETED_TRAVERSAL) {
if (state == PENDING_BOTH_CHILDREN) {
if (currentNode == p || currentNode == q) {
if (isOneNodeFound) {
return ancestor;
} else {
isOneNodeFound = true;
ancestor = traversalStack.peek().getKey();
}
}
nextNode = currentNode.left;
} else {
nextNode = currentNode.right;
}
traversalStack.pop();
traversalStack.push(new Pair<TreeNode, Integer>(currentNode, state - 1));
if (nextNode != null) {
traversalStack.push(new Pair<TreeNode, Integer>(nextNode, PENDING_BOTH_CHILDREN));
}
} else {
if (ancestor == traversalStack.pop().getKey() && isOneNodeFound) {
ancestor = traversalStack.peek().getKey();
}
}
}
return null;
}
}
The Java solution provided implements a method to find the Lowest Common Ancestor (LCA) of two nodes in a binary tree using an iterative depth-first search (DFS) approach. Here's a summary of the code functionality and structure:
- The solution uses a stack to manage the DFS traversal state of each node, utilizing pairs of a tree node and an integer state. The integer state represents the traversal progress for the node's children.
- A
Pair
object holds each node and its traversal state. Three states manage the traversal progress:PENDING_BOTH_CHILDREN
- both children of the node still need to be processed.DONE_LEFT_CHILD
- only the left child has been processed.COMPLETED_TRAVERSAL
- both children have been processed.
- The algorithm starts by pushing the root node with the
PENDING_BOTH_CHILDREN
state onto the stack. - The
isOneNodeFound
flag is used to indicate if one of the target nodes is found during the traversal. Theancestor
variable holds the last common ancestor encountered during the search. - The main loop processes each node according to its current state:
- If in the
PENDING_BOTH_CHILDREN
state and the current node is one of the nodes (p
orq
), the code checks theisOneNodeFound
flag. If the flag is already set, it returns the currentancestor
. If not, it sets the flag and updates theancestor
. - The node's children are pushed onto the stack appropriately based on the traversal state.
- If a node completes traversal (
COMPLETED_TRAVERSAL
), it updates theancestor
based on the stack's state.
- If in the
- The loop continues until the stack is empty. If both nodes are found, the
ancestor
is the LCA; otherwise, it returnsnull
if the LCA does not exist within the given part of the tree.
This method effectively tracks back on the path taken and updates possible ancestors as nodes are completely processed, ensuring non-recursive tree traversal management with explicit state tracking.
class AncestorFinder:
# Different states for traversal steps
UNVISITED = 2
LEFT_VISITED = 1
BOTH_VISITED = 0
def findLCA(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
# Stack to keep track of the navigation of nodes
traversal_stack = [(root, AncestorFinder.UNVISITED)]
# Flag to mark the encounter of either p or q
first_found = False
# Index of Lowest Common Ancestor in stack
LCA_index = -1
# Continue until all nodes are processed
while traversal_stack:
# Current node and its state
current_node, state = traversal_stack[-1]
# Process unless both children are visited
if state != AncestorFinder.BOTH_VISITED:
# If this is first traversal to the node
if state == AncestorFinder.UNVISITED:
# Check if the current_node is one of p or q
if current_node == p or current_node == q:
# If one is already found this means we have discovered both
if first_found:
return traversal_stack[LCA_index][0]
else:
# Mark the flag true, and note the current LCA index
first_found = True
LCA_index = len(traversal_stack) - 1
# First move to the left child
next_node = current_node.left
else:
# Move to the right child
next_node = current_node.right
# Mark this node as partially visited by decreasing the state
traversal_stack.pop()
traversal_stack.append((current_node, state - 1))
# If we have a valid child node, push it to stack
if next_node:
traversal_stack.append((next_node, AncestorFinder.UNVISITED))
else:
# If both children are visited, we can retract
if first_found and LCA_index == len(traversal_stack) - 1:
LCA_index -= 1
traversal_stack.pop()
return None
This solution implements a method to find the Lowest Common Ancestor (LCA) of two nodes in a binary tree using a manual stack for iterative traversal, which avoids recursion. Understand the Python implementation through these concise points:
- It defines a class
AncestorFinder
with an embedded methodfindLCA
, which takes the root of the tree and two nodes,p
andq
, as inputs. - The method utilizes three states (
UNVISITED
,LEFT_VISITED
,BOTH_VISITED
) to monitor the traversal status of each node during the process. - The core of the algorithm uses a stack,
traversal_stack
, to manage the state and node pairs as the tree is traversed. - Two flags,
first_found
andLCA_index
, help in identifying when both the required nodes are found and in determining the position of the lowest common ancestor in the stack. - The logic in
findLCA
involves:- Pushing nodes onto the stack until a node is found that matches either
p
orq
. - Marking the index of that node if it's the first of the two nodes discovered; if both are found, the LCA is determined from the stack based on the stored indices.
- Adjusting the states and traversing to left and right children as determined by conditional checks.
- Pushing nodes onto the stack until a node is found that matches either
In an iterative depth-first search manner, the algorithm ensures that it checks each node and its children, pushing and popping from the stack, until the LCA is identified or the stack is empty. If both nodes p
and q
are encountered during the traversal, the method returns the LCA; otherwise, it returns None
. Ensure robust exception handling and boundary checks when applying this solution in different scenarios.
No comments yet.