235. Lowest Common Ancestor of a Binary Search Tree
Problem Description
In this problem, we are given a binary search tree (BST). Our task is to find the lowest common ancestor (LCA) of two given nodes, p
and q
, within this BST. The lowest common ancestor is the furthest node from the root that is an ancestor of both nodes. This means that the LCA is the node from which both p
and q
are descended, and it could be either of the nodes themselves if one is an ancestor of the other.
Flowchart Walkthrough
Let's analyze this problem using the Flowchart to determine if Depth-First Search (DFS) is suitable for solving Leetcode 235: Lowest Common Ancestor of a Binary Search Tree. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A tree is a specific type of graph.
Is it a tree?
- Yes: More specifically, it is a Binary Search Tree (BST).
Is the problem related to directed acyclic graphs (DAGs)?
- No: The problem focuses on finding the lowest common ancestor in a BST, not a DAG.
Is the problem related to shortest paths?
- No: We are not looking for shortest paths; our goal is to locate the lowest common ancestor of two nodes in the BST.
Does the problem involve connectivity?
- No: Although a tree is inherently about connectivity (nodes connected by edges), the primary goal here isn’t to explore connectivity between all nodes, but rather to specifically find a common ancestor node.
Does the problem have small constraints?
- Yes: Despite this aspect not being as specific in the flowchart and assuming typical BST problems can be efficiently handled with tree traversals like DFS, given the nature of trees and their traversal techniques (in-order, pre-order, post-order).
Conclusion: According to the flowchart, and the outlined path:
- We start with a tree.
- We must apply DFS.
Therefore, the path concludes at applying Depth-First Search specifically suited to navigate through a tree's structure to locate the lowest common ancestor, efficiently respecting the properties inherent to a Binary Search Tree.
Intuition
The intuition behind the solution stems from the properties of a BST. In a BST, for any node n
, all nodes in the left subtree of n
have values less than n
, and all nodes in the right subtree have values greater than n
. When searching for the LCA of p
and q
, there are three possibilities:
- The values of both
p
andq
are less than the value of the current node. This means the LCA must be in the left subtree. - The values of both
p
andq
are greater than the value of the current node. This means the LCA must be in the right subtree. - One value is less than the current node's value and the other is greater (or one of them is equal to the current node's value). This means that the current node is the LCA because
p
andq
are in different subtrees.
The given solution iterates over the tree without using recursion. It repeatedly compares the current node's value with p
and q
values to determine the direction of the search or to conclude that the current node is the LCA.
Learn more about Tree, Depth-First Search, Binary Search Tree and Binary Tree patterns.
Solution Approach
In the provided solution, the algorithm uses iteration to navigate through the tree and find the LCA. It does so without the need for additional data structures, which keeps the space complexity at a constant O(1). There is also no recursion involved, which means the solution does not rely on the call stack and thus does not incur extra space cost due to recursion stack frames.
The algorithm can be broken down into the following steps:
- Start with the root of the BST as the current node.
- Compare the values of the current node (
root.val
) with the values ofp
andq
. - If both
p
andq
have values less thanroot.val
, the LCA must be in the left subtree. Therefore, update the current node to beroot.left
and continue the search. - If both
p
andq
have values greater thanroot.val
, the LCA must be in the right subtree. Therefore, update the current node to beroot.right
and continue the search. - If the value of
p
is less thanroot.val
and the value ofq
is greater thanroot.val
, or vice versa, this implies thatp
andq
lie in different subtrees of the current node. Thus, the current node must be their LCA. - In a case where either
p
orq
is equivalent toroot.val
, it means that eitherp
orq
is the LCA itself, as it is an ancestor of the other node (a node to be a descendant of itself
).
The algorithm continues the iteration until the condition in step 5 or 6 is met. Once either condition is met, it returns the current node, which is the LCA.
Here's a breakdown of the code:
while 1: # Start an indefinite loop that will break once LCA is found
if root.val < min(p.val, q.val): # Check if both nodes are in the right subtree
root = root.right
elif root.val > max(p.val, q.val): # Check if both nodes are in the left subtree
root = root.left
else:
return root # Found the LCA or one of the nodes is the LCA itself
The loop will always terminate because of the BST property which ensures that, with each step, the current node is moving closer to the LCA until it is found.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a binary search tree (BST) for our example:
6 / \ 2 8 / \ / \ 0 4 7 9 / \ 3 5
Assume we need to find the lowest common ancestor (LCA) of nodes p
with value 2 and q
with value 8.
Following the solution approach:
- We start with the root of the BST, which has a value of 6.
- We compare the values of
p
andq
to the current node (root), which is 6.p.val
is 2 andq.val
is 8.
- Since 6 is greater than 2 and less than 8, it is neither solely in the left subtree nor in the right subtree relative to both
p
andq
. Therefore, we have found the condition from step 5:p
andq
lie in different subtrees of the current node. - According to the algorithm, we have found the LCA because one node is in the left subtree and the other node is in the right subtree of node 6.
So, the lowest common ancestor of nodes 2 and 8 in this BST is the node with the value 6. The search stops here and we return the current node (root) as the LCA.
Solution Implementation
1# Definition for a binary tree node.
2class TreeNode:
3 def __init__(self, val):
4 self.val = val
5 self.left = None
6 self.right = None
7
8
9class Solution:
10 def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
11 # Loop indefinitely until the lowest common ancestor is found
12 while True:
13 # Find the smaller and larger values of p and q
14 smaller_value = min(p.val, q.val)
15 larger_value = max(p.val, q.val)
16
17 # If both p and q are on the right of the current node, move right
18 if root.val < smaller_value:
19 root = root.right
20 # If both p and q are on the left of the current node, move left
21 elif root.val > larger_value:
22 root = root.left
23 # If we are in a position where p and q are on different sides
24 # of root, or one of them is equal to root, then root is the LCA
25 else:
26 return root
27
1// Definition for a binary tree node.
2class TreeNode {
3 int val;
4 TreeNode left;
5 TreeNode right;
6
7 // Constructor to create a new node with a given value
8 TreeNode(int value) {
9 val = value;
10 }
11}
12
13class Solution {
14
15 // Function to find the lowest common ancestor of two nodes in a binary search tree
16 public TreeNode lowestCommonAncestor(TreeNode root, TreeNode firstNode, TreeNode secondNode) {
17 // Traverse the tree starting with the root
18 while (root != null) {
19 // If both nodes are greater than current node, search in right subtree
20 if (root.val < Math.min(firstNode.val, secondNode.val)) {
21 root = root.right; // Move to the right child
22 }
23 // If both nodes are less than current node, search in left subtree
24 else if (root.val > Math.max(firstNode.val, secondNode.val)) {
25 root = root.left; // Move to the left child
26 }
27 // We've found the lowest common ancestor node
28 else {
29 return root;
30 }
31 }
32 // In case the while loop exits without returning (it shouldn't in proper usage), return null
33 return null;
34 }
35}
36
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int value;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode(int x) : value(x), left(nullptr), right(nullptr) {}
8 * };
9 */
10
11class Solution {
12public:
13 // Function to find the lowest common ancestor in a binary search tree.
14 TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
15 // Loop until the lowest common ancestor is found.
16 while (true) {
17 // If both p and q are greater than root, then LCA must be in the right subtree.
18 if (root->value < min(p->value, q->value)) {
19 root = root->right;
20 }
21 // If both p and q are less than root, then LCA must be on the left subtree.
22 else if (root->value > max(p->value, q->value)) {
23 root = root->left;
24 }
25 // If we are in a state where p and q are on different sides of the root,
26 // or one of them is the root itself, we've found the LCA.
27 else {
28 return root;
29 }
30 }
31 }
32};
33
1// Definition of the binary tree node class interface to illustrate the structure of nodes in the binary tree.
2interface ITreeNode {
3 val: number;
4 left: ITreeNode | null;
5 right: ITreeNode | null;
6}
7
8/**
9 * Finds the lowest common ancestor (LCA) of two nodes in a binary search tree.
10 *
11 * @param root The root node of the binary search tree.
12 * @param nodeP The first node to find the LCA for.
13 * @param nodeQ The second node to find the LCA for.
14 * @returns The lowest common ancestor of nodeP and nodeQ.
15 */
16function lowestCommonAncestor(root: ITreeNode | null, nodeP: ITreeNode | null, nodeQ: ITreeNode | null): ITreeNode | null {
17 // Continue searching for the LCA as long as the current root is not null.
18 while (root) {
19 // If both nodes are smaller than the current root, LCA must be in the left subtree.
20 if (root.val > nodeP.val && root.val > nodeQ.val) {
21 root = root.left;
22 // If both nodes are larger than the current root, LCA must be in the right subtree.
23 } else if (root.val < nodeP.val && root.val < nodeQ.val) {
24 root = root.right;
25 // If we are in a situation where one node is on the left and the other is on the right,
26 // or one of the nodes is equal to the root, the current root is the LCA.
27 } else {
28 return root;
29 }
30 }
31
32 // If the root is null, it means we haven't found the LCA (which should not happen in a valid BST with both nodes present).
33 return null;
34}
35
Time and Space Complexity
The time complexity of the given code is O(H)
, where H
is the height of the binary tree. This is because the code traverses the tree from the root to the lowest common ancestor (LCA) without visiting any nodes more than once. In the worst case scenario, when the binary tree is unbalanced, the height H
could be linear in respect to the number of nodes, N
, resulting in a time complexity of O(N)
. However, in a balanced tree, time complexity is O(log N)
since the height of the tree is logarithmic in respect to the number of nodes.
The space complexity of the code is O(1)
regardless of the tree's structure, because it uses a fixed amount of space for pointers and without any recursive calls or additional data structures that depend on the size of the tree.
Learn more about how to find time and space complexity quickly using problem constraints.
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https algomonster s3 us east 2 amazonaws com cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS
Binary Search Tree Intro Definition A Binary Search Tree or BST is a rooted binary tree with the value of each internal node being greater than all the values in the respective node's left subtree and less than the ones in its right subtree An empty tree is a BST since it satisfies the
Want a Structured Path to Master System Design Too? Don’t Miss This!