993. Cousins in Binary Tree
Problem Description
You are given a binary tree with unique node values and two values x
and y
that exist in the tree. Your task is to determine if the nodes containing these values are cousins.
Two nodes are considered cousins if they meet both of these conditions:
- They are at the same depth in the tree
- They have different parents
The depth of a node is defined as:
- The root node is at depth
0
- Children of a node at depth
k
are at depthk + 1
The solution uses a breadth-first search (BFS) approach with a queue to traverse the tree level by level. For each node visited, it tracks both the node and its parent. The algorithm:
- Starts with the root node (which has no parent) in the queue
- Processes each level of the tree completely before moving to the next level
- When it finds node
x
, it records its parent (p1
) and depth (d1
) - When it finds node
y
, it records its parent (p2
) and depth (d2
) - For each node processed, adds its children to the queue along with the current node as their parent
- After traversing the entire tree, checks if
x
andy
have different parents (p1 != p2
) and the same depth (d1 == d2
)
The function returns true
if the nodes are cousins (same depth, different parents), and false
otherwise.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph with nodes (tree nodes) and edges (parent-child relationships).
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure.
DFS
- Yes: We arrive at DFS as our algorithm choice for this tree problem.
Conclusion: The flowchart suggests using a DFS (Depth-First Search) approach for solving the Cousins in Binary Tree problem.
Why DFS Works Here
DFS is particularly suitable for this problem because:
- Tree Traversal: We need to traverse the entire tree to find both nodes
x
andy
- Parent Tracking: During DFS traversal, we can naturally track the parent of each node as we recursively explore
- Depth Tracking: The recursive call stack in DFS inherently tracks the depth - each recursive call represents going one level deeper
Alternative DFS Implementation
While the provided solution uses BFS, a DFS approach would work equally well:
def isCousins(self, root: Optional[TreeNode], x: int, y: int) -> bool:
def dfs(node, parent, depth, target):
if not node:
return None, None
if node.val == target:
return parent, depth
left_result = dfs(node.left, node, depth + 1, target)
if left_result[0] is not None:
return left_result
return dfs(node.right, node, depth + 1, target)
x_parent, x_depth = dfs(root, None, 0, x)
y_parent, y_depth = dfs(root, None, 0, y)
return x_parent != y_parent and x_depth == y_depth
Both DFS and BFS are valid approaches for this tree traversal problem, with the flowchart correctly identifying DFS as a suitable pattern for tree-based problems.
Intuition
To determine if two nodes are cousins, we need to check two things: whether they're at the same depth and whether they have different parents. This immediately tells us we need to track both the depth and parent information for each node as we traverse the tree.
Think about how you would manually check if two people are cousins in a family tree - you'd trace back to see if they're at the same generation level (depth) and confirm they have different immediate parents. The same logic applies here.
The key insight is that we need to explore the tree systematically to find both target nodes while keeping track of their relationships. When we visit each node, we need to remember:
- Who is its parent? (to check if they have different parents)
- How deep is it in the tree? (to check if they're at the same level)
Whether we use BFS or DFS, the core idea remains the same: traverse the tree while maintaining parent-child relationships. BFS naturally processes nodes level by level, making depth tracking straightforward with a depth counter. DFS uses the recursive call stack where each recursive level represents one step deeper into the tree.
The solution becomes clear: find both nodes x
and y
in the tree, record their parents and depths, then check if they satisfy the cousin criteria: same depth AND different parents
. If we find that x
and y
have the same parent, they're siblings, not cousins. If they're at different depths, they're not cousins either. Only when both conditions are met (same depth, different parents) do we return true
.
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The implementation uses a Breadth-First Search (BFS) approach with a queue to traverse the tree level by level. Here's how the solution works:
Data Structures Used:
- A
deque
(double-ended queue) to store tuples of(node, parent)
pairs - Variables to track parents (
p1
,p2
) and depths (d1
,d2
) of nodesx
andy
Algorithm Steps:
-
Initialize the queue with the root node and
None
as its parent:q = deque([(root, None)])
-
Set up tracking variables:
depth = 0
to track the current levelp1, p2 = None, None
to store parents ofx
andy
d1, d2 = None, None
to store depths ofx
andy
-
Process nodes level by level:
while q: for _ in range(len(q)): # Process all nodes at current level
The inner loop ensures we process all nodes at the same depth before incrementing the depth counter.
-
For each node, check if it matches our targets:
node, parent = q.popleft() if node.val == x: p1, d1 = parent, depth elif node.val == y: p2, d2 = parent, depth
When we find either target value, we record its parent and current depth.
-
Add children to the queue for next level:
if node.left: q.append((node.left, node)) if node.right: q.append((node.right, node))
Each child is added with the current node as its parent.
-
Increment depth after processing each level:
depth += 1
-
Return the result:
return p1 != p2 and d1 == d2
Returns
True
only if both conditions are met:- Different parents (
p1 != p2
) - Same depth (
d1 == d2
)
- Different parents (
Time Complexity: O(n)
where n
is the number of nodes, as we visit each node once.
Space Complexity: O(w)
where w
is the maximum width of the tree, which is the maximum number of nodes at any level (worst case O(n/2)
for a complete binary tree).
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate how the BFS solution finds cousin nodes.
Consider this binary tree where we want to check if nodes with values 4
and 3
are cousins:
1 / \ 2 3 / 4
Initial Setup:
- Target values:
x = 4
,y = 3
- Queue starts with:
[(1, None)]
depth = 0
,p1 = None
,p2 = None
,d1 = None
,d2 = None
Level 0 (Root):
- Process node
1
with parentNone
- Check:
1 ≠ 4
and1 ≠ 3
, so no match - Add children to queue:
[(2, 1), (3, 1)]
- Increment
depth = 1
Level 1:
-
Process node
2
with parent1
- Check:
2 ≠ 4
and2 ≠ 3
, no match - Add left child: queue becomes
[(3, 1), (4, 2)]
- Check:
-
Process node
3
with parent1
- Check:
3 = y
! Recordp2 = 1
,d2 = 1
- No children to add
- Check:
-
After processing level 1, increment
depth = 2
Level 2:
- Process node
4
with parent2
- Check:
4 = x
! Recordp1 = 2
,d1 = 2
- No children to add
- Check:
- Queue is now empty
Final Check:
p1 = 2
,p2 = 1
(different parents ✓)d1 = 2
,d2 = 1
(different depths ✗)- Return
False
- they are NOT cousins
Let's try another example where nodes ARE cousins:
1 / \ 2 3 / \ \ 4 5 6
Checking if 4
and 6
are cousins:
Level 0: Process node 1
, add children 2
and 3
Level 1: Process nodes 2
and 3
, add their children 4
, 5
, and 6
Level 2:
- Find node
4
with parent2
at depth2
- Find node
6
with parent3
at depth2
Result:
- Different parents (
2 ≠ 3
) ✓ - Same depth (
2 = 2
) ✓ - Return
True
- they ARE cousins!
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from collections import deque
9from typing import Optional
10
11class Solution:
12 def isCousins(self, root: Optional[TreeNode], x: int, y: int) -> bool:
13 """
14 Determine if two nodes are cousins in a binary tree.
15 Two nodes are cousins if they have the same depth but different parents.
16
17 Args:
18 root: Root node of the binary tree
19 x: Value of the first node
20 y: Value of the second node
21
22 Returns:
23 True if x and y are cousins, False otherwise
24 """
25 # Initialize queue for BFS with (node, parent) tuples
26 queue = deque([(root, None)])
27
28 # Track current depth level
29 current_depth = 0
30
31 # Variables to store parent and depth information for x and y
32 parent_x = parent_y = None
33 depth_x = depth_y = None
34
35 # Perform level-order traversal (BFS)
36 while queue:
37 # Process all nodes at the current depth level
38 level_size = len(queue)
39
40 for _ in range(level_size):
41 node, parent = queue.popleft()
42
43 # Check if current node matches x or y
44 if node.val == x:
45 parent_x = parent
46 depth_x = current_depth
47 elif node.val == y:
48 parent_y = parent
49 depth_y = current_depth
50
51 # Add children to queue for next level
52 if node.left:
53 queue.append((node.left, node))
54 if node.right:
55 queue.append((node.right, node))
56
57 # Move to next depth level
58 current_depth += 1
59
60 # Cousins must have different parents but same depth
61 return parent_x != parent_y and depth_x == depth_y
62
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 /**
18 * Determines if two nodes in a binary tree are cousins.
19 * Two nodes are cousins if they have the same depth but different parents.
20 *
21 * @param root The root of the binary tree
22 * @param x The value of the first node
23 * @param y The value of the second node
24 * @return true if nodes with values x and y are cousins, false otherwise
25 */
26 public boolean isCousins(TreeNode root, int x, int y) {
27 // Queue for BFS traversal, storing pairs of [node, parent]
28 Deque<TreeNode[]> queue = new ArrayDeque<>();
29 queue.offer(new TreeNode[] {root, null});
30
31 // Variables to track depth and parent of nodes x and y
32 int depthX = 0;
33 int depthY = 0;
34 TreeNode parentX = null;
35 TreeNode parentY = null;
36
37 // Perform level-order traversal
38 for (int currentDepth = 0; !queue.isEmpty(); currentDepth++) {
39 int levelSize = queue.size();
40
41 // Process all nodes at the current level
42 for (int i = 0; i < levelSize; i++) {
43 TreeNode[] pair = queue.poll();
44 TreeNode currentNode = pair[0];
45 TreeNode parentNode = pair[1];
46
47 // Check if current node matches x or y
48 if (currentNode.val == x) {
49 depthX = currentDepth;
50 parentX = parentNode;
51 } else if (currentNode.val == y) {
52 depthY = currentDepth;
53 parentY = parentNode;
54 }
55
56 // Add left child to queue if it exists
57 if (currentNode.left != null) {
58 queue.offer(new TreeNode[] {currentNode.left, currentNode});
59 }
60
61 // Add right child to queue if it exists
62 if (currentNode.right != null) {
63 queue.offer(new TreeNode[] {currentNode.right, currentNode});
64 }
65 }
66 }
67
68 // Nodes are cousins if they have different parents but same depth
69 return parentX != parentY && depthX == depthY;
70 }
71}
72
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 /**
15 * Determines if two nodes are cousins in a binary tree.
16 * Two nodes are cousins if they have the same depth but different parents.
17 *
18 * @param root The root of the binary tree
19 * @param x Value of the first node
20 * @param y Value of the second node
21 * @return true if x and y are cousins, false otherwise
22 */
23 bool isCousins(TreeNode* root, int x, int y) {
24 // Queue to store pairs of (current node, parent node) for BFS traversal
25 queue<pair<TreeNode*, TreeNode*>> bfsQueue;
26 bfsQueue.push({root, nullptr});
27
28 // Variables to store depth and parent information for nodes x and y
29 int depthX = 0, depthY = 0;
30 TreeNode* parentX = nullptr;
31 TreeNode* parentY = nullptr;
32
33 // Level-order traversal (BFS) to find nodes x and y
34 for (int currentDepth = 0; !bfsQueue.empty(); ++currentDepth) {
35 // Process all nodes at the current depth level
36 int nodesAtCurrentLevel = bfsQueue.size();
37
38 for (int i = 0; i < nodesAtCurrentLevel; ++i) {
39 // Get current node and its parent from queue
40 auto [currentNode, parentNode] = bfsQueue.front();
41 bfsQueue.pop();
42
43 // Check if current node matches x or y
44 if (currentNode->val == x) {
45 depthX = currentDepth;
46 parentX = parentNode;
47 } else if (currentNode->val == y) {
48 depthY = currentDepth;
49 parentY = parentNode;
50 }
51
52 // Add left child to queue if it exists
53 if (currentNode->left) {
54 bfsQueue.push({currentNode->left, currentNode});
55 }
56
57 // Add right child to queue if it exists
58 if (currentNode->right) {
59 bfsQueue.push({currentNode->right, currentNode});
60 }
61 }
62 }
63
64 // Nodes are cousins if they have same depth but different parents
65 return depthX == depthY && parentX != parentY;
66 }
67};
68
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Determines if two nodes in a binary tree are cousins.
17 * Two nodes are cousins if they are at the same depth but have different parents.
18 *
19 * @param root - The root node of the binary tree
20 * @param x - The value of the first node
21 * @param y - The value of the second node
22 * @returns true if nodes with values x and y are cousins, false otherwise
23 */
24function isCousins(root: TreeNode | null, x: number, y: number): boolean {
25 // Initialize depth and parent for both target nodes
26 let xDepth: number = 0;
27 let yDepth: number = 0;
28 let xParent: TreeNode | null = null;
29 let yParent: TreeNode | null = null;
30
31 // BFS queue storing [current node, parent node] pairs
32 const queue: Array<[TreeNode, TreeNode | null]> = [[root!, null]];
33
34 // Perform level-order traversal
35 for (let currentDepth = 0; queue.length > 0; currentDepth++) {
36 // Store nodes for the next level
37 const nextLevel: Array<[TreeNode, TreeNode | null]> = [];
38
39 // Process all nodes at the current depth
40 for (const [currentNode, parentNode] of queue) {
41 // Check if current node matches x or y
42 if (currentNode.val === x) {
43 xDepth = currentDepth;
44 xParent = parentNode;
45 } else if (currentNode.val === y) {
46 yDepth = currentDepth;
47 yParent = parentNode;
48 }
49
50 // Add left child to next level if it exists
51 if (currentNode.left) {
52 nextLevel.push([currentNode.left, currentNode]);
53 }
54
55 // Add right child to next level if it exists
56 if (currentNode.right) {
57 nextLevel.push([currentNode.right, currentNode]);
58 }
59 }
60
61 // Replace current queue with next level nodes
62 queue.splice(0, queue.length, ...nextLevel);
63 }
64
65 // Nodes are cousins if they have the same depth but different parents
66 return xDepth === yDepth && xParent !== yParent;
67}
68
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of nodes in the binary tree.
The algorithm uses BFS (Breadth-First Search) to traverse the tree level by level. Each node in the tree is visited exactly once - we add it to the queue once and process it once when we pop it from the queue. For each node, we perform constant time operations: checking if its value equals x
or y
, and adding its children to the queue. Therefore, the overall time complexity is linear with respect to the number of nodes.
Space Complexity: O(w)
where w
is the maximum width of the tree.
The space complexity is determined by the maximum size of the queue at any point during the traversal. In BFS, the queue stores all nodes at the current level being processed. The maximum number of nodes at any level is the width of the tree. In the worst case (a complete binary tree), the maximum width occurs at the last level and can be up to n/2
nodes, making the space complexity O(n)
in the worst case. In the best case (a skewed tree), the width is 1, making the space complexity O(1)
. Generally, we express this as O(w)
where w
represents the maximum width of the tree.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Early Termination Optimization Gone Wrong
A common pitfall is trying to optimize by returning early once both x
and y
are found at the same level:
Incorrect Approach:
while queue:
level_size = len(queue)
found_x = found_y = False
parent_x = parent_y = None
for _ in range(level_size):
node, parent = queue.popleft()
if node.val == x:
found_x = True
parent_x = parent
elif node.val == y:
found_y = True
parent_y = parent
# Early termination - WRONG!
if found_x and found_y:
return parent_x != parent_y
if node.left:
queue.append((node.left, node))
if node.right:
queue.append((node.right, node))
Why it fails: This breaks the level-order traversal by returning before processing all nodes at the current level. You might miss adding children to the queue, and the check happens before the level is fully processed.
Correct Approach: Check after processing the entire level:
while queue:
level_size = len(queue)
found_x = found_y = False
parent_x = parent_y = None
for _ in range(level_size):
node, parent = queue.popleft()
if node.val == x:
found_x = True
parent_x = parent
elif node.val == y:
found_y = True
parent_y = parent
if node.left:
queue.append((node.left, node))
if node.right:
queue.append((node.right, node))
# Check after the entire level is processed
if found_x and found_y:
return parent_x != parent_y
# If only one is found at this level, they can't be cousins
if found_x or found_y:
return False
2. Parent-Child Confusion
Another pitfall is checking if x
and y
are parent and child of each other, thinking this automatically means they're not cousins:
Incorrect Additional Check:
# Trying to handle parent-child case explicitly - UNNECESSARY! if node.val == x: if node.left and node.left.val == y: return False if node.right and node.right.val == y: return False
Why it's unnecessary: The BFS approach already handles this correctly. If x
is the parent of y
, they will be at different depths, so depth_x != depth_y
will make the function return False
.
3. Using Node References Instead of Values
A subtle mistake is storing node references instead of parent references:
Incorrect:
if node.val == x: parent_x = node # WRONG - storing the node itself depth_x = current_depth
Correct:
if node.val == x: parent_x = parent # Correct - storing the parent reference depth_x = current_depth
This would cause incorrect comparisons since you'd be comparing the nodes themselves rather than their parents.
Which of the following shows the order of node visit in a Breadth-first Search?
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 assets algo monster 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 As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!