Cousins in Binary Tree
EasyPrompt
Given the root of a binary tree with unique values and the values of two different nodes of the tree x and y, return true if the nodes corresponding to the values x and y in the tree are cousins, or false otherwise.
Two nodes of a binary tree are cousins if they have the same depth with different parents.
Note that in a binary tree, the root node is at the depth 0, and children of each depth k node are at the depth k + 1.
Example 1:
Input: root = [1,2,3,4], x = 4, y = 3
Output: falseExample 2:
Input: root = [1,2,3,null,4,null,5], x = 5, y = 4
Output: trueExample 3:
Input: root = [1,2,3,null,4], x = 2, y = 3
Output: false
Constraints:
- The number of nodes in the tree is in the range
[2, 100]. 1 <= Node.val <= 100- Each node has a unique value.
x != yxandyare exist in the tree.
Approaches
3 approaches with complexity analysis and trade-offs.
This straightforward approach involves two independent traversals of the tree. First, we traverse the tree to find the depth and parent of node x. Then, we perform a second, separate traversal to find the depth and parent of node y. Once we have this information for both nodes, we can easily check if they satisfy the conditions for being cousins (same depth, different parents).
Algorithm
- Create a helper class or structure to hold the result of a search, containing the parent node and the depth.
- Define a recursive helper function,
findNode(node, parent, depth, target), that performs a DFS to find thetargetvalue. - If the current
nodeis null, return null. - If
node.valmatches thetarget, create and return a new result object with the currentparentanddepth. - Recursively call
findNodeon the left child. If it returns a non-null result, propagate that result up. - Otherwise, recursively call
findNodeon the right child and return its result. - In the main
isCousinsfunction, call the helper function once to find the info forx. - Call the helper function a second time to find the info for
y. - Compare the results: return
trueifinfoX.depth == infoY.depthandinfoX.parent != infoY.parent.
Walkthrough
The core idea is to isolate the problem of finding a node's information. We create a helper function that takes the root and a target value and returns the target's depth and its immediate parent. This function can be implemented using a standard Depth-First Search (DFS). The main function then orchestrates the process by calling this helper twice, once for x and once for y, and then comparing the returned information to make the final decision.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { // A simple class to hold the result of a search class NodeInfo { TreeNode parent; int depth; NodeInfo(TreeNode p, int d) { this.parent = p; this.depth = d; } } public boolean isCousins(TreeNode root, int x, int y) { NodeInfo infoX = findNode(root, null, 0, x); NodeInfo infoY = findNode(root, null, 0, y); return infoX.depth == infoY.depth && infoX.parent != infoY.parent; } private NodeInfo findNode(TreeNode node, TreeNode parent, int depth, int target) { if (node == null) { return null; } if (node.val == target) { return new NodeInfo(parent, depth); } NodeInfo leftResult = findNode(node.left, node, depth + 1, target); if (leftResult != null) { return leftResult; } return findNode(node.right, node, depth + 1, target); }}Complexity
Time
O(N). In the worst case, we traverse the entire tree twice, once for `x` and once for `y`. The complexity is O(N) + O(N) = O(N).
Space
O(H), where H is the height of the tree. This space is used by the recursion stack. In the worst case of a skewed tree, this can be O(N).
Trade-offs
Pros
Simple to conceptualize and implement.
The logic for finding a node is cleanly separated from the main comparison logic.
Cons
Inefficient as it may traverse the tree twice. If
xandyare in different subtrees, each search might traverse a large portion of the tree.
This approach improves upon the previous one by finding the required information for both nodes, x and y, in a single pass through the tree. We can perform a single Depth-First Search (DFS) traversal and keep track of the parent and depth of x and y as we encounter them.
Algorithm
- Initialize class-level variables to store the parent and depth for
xandy(e.g.,parentX,depthX,parentY,depthY). - Create a recursive DFS helper function,
dfs(node, parent, depth). - The function traverses the tree. When it encounters
xory, it records their respective parent and depth in the class-level variables. - If
node.val == x, updateparentXanddepthX. - If
node.val == y, updateparentYanddepthY. - Add an optimization to stop the traversal once both
xandyhave been found. - Recursively call
dfsfor the left and right children, passingnodeas the new parent anddepth + 1as the new depth. - After the initial call to
dfsreturns, compare the recorded values: returntrueifdepthX == depthYandparentX != parentY.
Walkthrough
Instead of traversing the tree twice, we can gather all the necessary information in one go. We perform a single DFS from the root. During the traversal, we pass down the current node's parent and its depth. When we find either x or y, we store their parent and depth information in shared variables. After the traversal is complete, we will have the information for both nodes and can perform the final check.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { private TreeNode parentX; private TreeNode parentY; private int depthX = -1; private int depthY = -1; public boolean isCousins(TreeNode root, int x, int y) { dfs(root, null, 0, x, y); return depthX == depthY && parentX != parentY; } private void dfs(TreeNode node, TreeNode parent, int depth, int x, int y) { if (node == null) { return; } if (node.val == x) { parentX = parent; depthX = depth; } if (node.val == y) { parentY = parent; depthY = depth; } // Optimization: if both found, no need to go deeper if (depthX != -1 && depthY != -1) { return; } dfs(node.left, node, depth + 1, x, y); // Optimization: if both found after left traversal, no need to traverse right if (depthX != -1 && depthY != -1) { return; } dfs(node.right, node, depth + 1, x, y); }}Complexity
Time
O(N), as we traverse each node at most once.
Space
O(H), where H is the height of the tree, for the recursion stack. In the worst case of a skewed tree, this is O(N).
Trade-offs
Pros
More efficient than two separate traversals as it only requires a single pass over the tree.
The logic is still relatively simple to follow.
Cons
Uses class-level variables to maintain state across recursive calls, which can sometimes be less clean than passing state through parameters or return values.
For very deep, skewed trees, the recursion depth could lead to a stack overflow error (though unlikely with the given constraints).
Since the definition of cousins is based on depth, a Breadth-First Search (BFS) is a very natural and efficient approach. BFS explores the tree level by level. We can iterate through the tree one level at a time and check if both x and y appear on the same level, while also ensuring they don't share the same parent.
Algorithm
- Initialize a queue for level-order traversal and add the
root. - Loop while the queue is not empty.
- In each iteration, first get the number of nodes at the current level (
levelSize). - Initialize boolean flags
foundX = falseandfoundY = falsefor the current level. - Loop
levelSizetimes to process all nodes on the current level.- Dequeue a
node. - Sibling Check: Check if the current
nodeis the parent of bothxandy. Ifnode.leftandnode.rightcontainxandy, they are siblings, so returnfalseimmediately. - Check if the
node's value isxoryand update the corresponding boolean flag. - Enqueue the non-null children of the
nodefor the next level.
- Dequeue a
- After processing the level, check the flags:
- If
foundXandfoundYare both true, they are on the same level and are not siblings (due to the earlier check). Returntrue. - If only one of
foundXorfoundYis true, they are at different depths. Returnfalse.
- If
- In each iteration, first get the number of nodes at the current level (
- If the loop completes without returning, it means the nodes were not found as cousins. Return
false.
Walkthrough
This iterative approach uses a queue to explore the tree layer by layer. The key insight is to process all nodes at a given depth before moving to the next depth. For each level, we check for the presence of x and y. A crucial part of this approach is checking for the sibling condition: before we even check if a node's value is x or y, we check if its children are x and y. If they are, we know they share a parent and can immediately conclude they are not cousins.
If we finish a level and find that both x and y were present, we know they have the same depth and are not siblings, so they must be cousins. If only one was found, they are at different depths, so they cannot be cousins. If neither was found, we continue to the next level.
import java.util.LinkedList;import java.util.Queue; /** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public boolean isCousins(TreeNode root, int x, int y) { if (root == null) { return false; } Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { int levelSize = queue.size(); boolean foundX = false; boolean foundY = false; // Process one level at a time for (int i = 0; i < levelSize; i++) { TreeNode node = queue.poll(); // Check if x and y are children of the same node (siblings) if (node.left != null && node.right != null) { if ((node.left.val == x && node.right.val == y) || (node.left.val == y && node.right.val == x)) { return false; } } if (node.val == x) foundX = true; if (node.val == y) foundY = true; if (node.left != null) queue.offer(node.left); if (node.right != null) queue.offer(node.right); } if (foundX && foundY) return true; // Found on same level, not siblings if (foundX || foundY) return false; // Found on different levels } return false; }}Complexity
Time
O(N), as each node is visited exactly once.
Space
O(W), where W is the maximum width of the tree. This space is used by the queue. In the worst case of a complete binary tree, W can be up to N/2, making the space complexity O(N).
Trade-offs
Pros
Very efficient as it can terminate early as soon as the conditions are met or violated.
Conceptually clean and a natural fit for problems involving levels/depth.
Iterative approach avoids potential stack overflow issues on very deep trees.
Cons
The space complexity can be O(N) for wide, complete binary trees, which might be worse than DFS's O(H) for very deep, narrow trees.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private int x , y ; private TreeNode p1 , p2 ; private int d1 , d2 ; public boolean isCousins ( TreeNode root , int x , int y ) { this . x = x ; this . y = y ; dfs ( root , null , 0 ); return p1 != p2 && d1 == d2 ; } private void dfs ( TreeNode root , TreeNode p , int d ) { if ( root == null ) { return ; } if ( root . val == x ) { p1 = p ; d1 = d ; } if ( root . val == y ) { p2 = p ; d2 = d ; } dfs ( root . left , root , d + 1 ); dfs ( root . right , root , d + 1 ); } }/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: bool isCousins ( TreeNode * root , int x , int y ) { TreeNode * p1 , * p2 ; int d1 , d2 ; function < void ( TreeNode * , TreeNode * , int ) > dfs = [ & ]( TreeNode * root , TreeNode * fa , int d ) { if ( ! root ) { return ; } if ( root -> val == x ) { p1 = fa ; d1 = d ; } if ( root -> val == y ) { p2 = fa ; d2 = d ; } dfs ( root -> left , root , d + 1 ); dfs ( root -> right , root , d + 1 ); }; dfs ( root , nullptr , 0 ); return p1 != p2 && d1 == d2 ; } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution : def isCousins ( self , root : Optional [ TreeNode ], x : int , y : int ) -> bool : def dfs ( root , fa , d ): if root is None : return if root . val == x : t [ 0 ] = ( fa , d ) if root . val == y : t [ 1 ] = ( fa , d ) dfs ( root . left , root , d + 1 ) dfs ( root . right , root , d + 1 ) t = [ None , None ] dfs ( root , None , 0 ) return t [ 0 ][ 0 ] != t [ 1 ][ 0 ] and t [ 0 ][ 1 ] == t [ 1 ][ 1 ]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.