DSA sheet · Trees · BFS pattern (also solved with DFS)

Cousins in Binary Tree

This problem sits in the BFS pattern of the sheet, but the teacher solves it both ways. First with DFS, which she calls the friendlier version for beginners. Then with BFS, which she calls the best approach, because it can stop early, as soon as it reaches the level where x and y live. The problem is a nice "family relations" question: it teaches you to collect two facts about a node (its level and its parent) and then compare them.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Family words you need first

Think of the tree as a family tree. The root is the oldest person, and every child hangs below its parent.

wordmeaning in a tree
level (or depth)How many steps down from the root a node is. The root is at level 0, its children at level 1, their children at level 2, and so on.
parentThe node directly above a node (just one step up). Not the grandparent, not any older ancestor.
siblingsTwo nodes with the same parent (brother and sister). They are always on the same level.
cousinsTwo nodes on the same level but with different parents.
uncle / nephewTwo nodes on different levels, like 3 and 4 in the picture below. They are related, but they are not cousins.

We will use this tree for most of the page:

        1          level 0
       / \
      2   3        level 1
     /   / \
    4   6   7      level 2
     \
      5            level 3
Doubt: 4 and 6 both come from 1 in the end. Isn't 1 their "common parent", so they have the same parent?
→ No. The teacher stresses this: "parent" means only the node one step up. 4's parent is 2 and 6's parent is 3. Every pair of nodes shares some ancestor higher up (at worst the root), so that can't be what "parent" means here.

Part A · Cousins with DFS

LeetCode 993

1The question in simple words

You get the root of a binary tree and two numbers x and y. Each is the value of some node in the tree. Return True if those two nodes are cousins, otherwise False.

Cousins need both of these:

x = 4, y = 3 → False
      1
     / \
    2   3
   /
  4
(different levels)
x = 2, y = 3 → False
      1
     / \
    2   3

(same level,
 same parent 1)
x = 4, y = 6 → True
        1
       / \
      2   3
     /   / \
    4   6   7
(same level,
 parents 2 and 3)

2What the constraints tell us

3Intuition: what we need to know at the end

Imagine you are a detective with a notebook that has four empty boxes:

x levelx parenty levely parent
????

Once all four boxes are filled, the answer is just one line:

x level == y level and x parent != y parent → True, else False

So the whole job is: walk the tree and fill these four boxes. In DFS we walk deep first. While walking, every node needs to know two things about itself: what level it is on, and who its parent is.

4Building the logic step by step

What do we check at each node?

Standing at a node, the teacher asks it two questions: "Does your value match x?" and "Does your value match y?"

Why the function needs extra parameters

A node only knows its value and its two children. It does not know its own level, and it does not know who its parent is. So the caller has to hand this information down:

Doubt 1: when the node matches x, why do we store the parent and not the node itself?
→ Because the cousin rule compares parents. Storing the matching node is useless for that. That's exactly why the parent has to travel down as a parameter: by the time we reach the match, it's the only way to know who's above it.

The base case

If the node is None, there is nothing to check and no children to visit → just return. This is what stops the recursion at the bottom of the tree.

Can we stop as soon as we find x?

No. Say we are looking for 4 and 6. We reach 4 first and fill x's boxes. But we have no idea where 6 is. It might be below 4, or somewhere on the right side. So we must keep going.

But once both x and y are found, all four boxes are full and there is nothing left to learn. So the teacher adds one more check: if all the boxes are already filled, return right away.

Doubt 2: does this early stop make DFS faster in Big-O?
→ Not in the worst case. The teacher's example: if x is the leftmost leaf and y is the rightmost leaf, DFS has to walk through basically the whole tree before it meets y. So the worst case is still O(n). The early stop only helps when x and y are found early.

What if x or y is never found?

Then its level box stays at its starting value (we use −1). The levels won't match, so the answer is False. (The constraints promise both exist, but the code is safe anyway.)

5Approach steps

  1. Create four variables: x level, x parent, y level, y parent (levels start at −1, meaning "not found yet").
  2. Call dfs(root, level=0, parent=None).
  3. In dfs: if the node is None → return. If both x and y are already found → return.
  4. If node value == x → save this level and this parent for x.
  5. If node value == y → save this level and this parent for y.
  6. Call dfs on the left child and on the right child with level + 1 and parent = this node.
  7. After dfs finishes: return x level == y level and x parent is not y parent.

6Code (Python)

Cousins with DFS
class Solution:
    def isCousins(self, root, x, y):
        self.x_level = -1        # -1 means "not found yet"
        self.x_parent = None
        self.y_level = -1
        self.y_parent = None
        self.dfs(root, 0, None, x, y)    # root: level 0, no parent
        return self.x_level == self.y_level and self.x_parent is not self.y_parent

    def dfs(self, root, level, parent, x, y):
        if root is None:                              # base case
            return
        if self.x_level != -1 and self.y_level != -1: # both found, stop early
            return
        if root.val == x:
            self.x_level = level
            self.x_parent = parent
        if root.val == y:
            self.y_level = level
            self.y_parent = parent
        self.dfs(root.left, level + 1, root, x, y)    # I am my child's parent
        self.dfs(root.right, level + 1, root, x, y)

7Code line by line

linewhat it means
self.x_level = -1 ...The four empty boxes. We keep them on self so every recursive call writes into the same boxes.
self.dfs(root, 0, None, x, y)Start at the root. Its level is 0 and it has no parent.
return self.x_level == self.y_level and self.x_parent is not self.y_parentThe cousin rule: same level and different parents. is not compares node objects, which is what we want, since two different parents could in theory share a value in other problems.
if root is None: returnFell off the tree. Nothing to check.
if self.x_level != -1 and self.y_level != -1: returnBoth boxes are filled, so no need to look further. Saves work when x and y are found early.
if root.val == x: ...Found x: write down the level and the parent we were handed.
if root.val == y: ...Found y: same thing for y.
self.dfs(root.left, level + 1, root, x, y)Go one level deeper on the left. The current node is the child's parent.
self.dfs(root.right, level + 1, root, x, y)Same for the right child.

8Dry run using the call stack

Tree from Part 0, with x = 4, y = 6. Each call is written as (node, level, parent). N = None.

        1
       / \
      2   3
     /   / \
    4   6   7
     \
      5
  1. Call (1, 0, N): 1 is not 4 and not 6 → go left. (1,0,N) waits on the stack.
  2. Call (2, 1, 1): no match → go left.
  3. Call (4, 2, 2): 4 == x → x level = 2, x parent = node 2. We can't stop yet, because y hasn't been found.
  4. 4's left is None → that call returns at once.
  5. 4's right: call (5, 3, 4): no match. Its left and right are None → both return. (5,3,4) is done and leaves the stack.
  6. (4,2,2) has finished both sides → leaves the stack. Back in (2,1,1), its right child is None → returns. (2,1,1) leaves the stack.
  7. Back in (1,0,N), now go right: call (3, 1, 1): no match → go left.
  8. Call (6, 2, 3): 6 == y → y level = 2, y parent = node 3. All four boxes are full now.
  9. 6's children are None → return. Back in (3,1,1), go right: call (7, 2, 3) → the "both found" check fires → return without looking at 7.
  10. Everything unwinds. Boxes: x (level 2, parent 2), y (level 2, parent 3). Same level ✓, different parents ✓ → True.
step 3: x found
(1,0,N)(2,1,1)(4,2,2) x!
step 5: deepest point
(1,0,N)(2,1,1)(4,2,2)(5,3,4)
step 8: y found
(1,0,N)(3,1,1)(6,2,3) y!
after stepx levelx parenty levely parent
start−1None−1None
322−1None
82223

Try x = 6, y = 7 yourself: both are level 2 but the parent is 3 for both → False. Try x = 4, y = 3: levels 2 and 1 → False.

9Complexity & remember

Remember DFS cousinsCarry level and parent down as parameters. Fill 4 boxes: x level, x parent, y level, y parent. At the end: same level AND different parent.

Part B · Cousins with BFS

Same question. Now we go level by level with a queue. The teacher's claim: BFS is the better approach here, because cousins always sit on the same level, and BFS handles one whole level at a time.

1The question in simple words

Same as Part A: return True if x and y are on the same level with different parents.

2What the constraints tell us

3Intuition

In level-order BFS, the queue holds one full level at a time. So:

So we look for the False case (siblings) at the parent, and the True case (both on one level) at the end of each level.

4Building the conditions from examples

Example 1: x = 4, y = 6 → True, found then and there

The levels are [1], then [2, 3], then [4, 6, 7], then [5]. While handling the level [4, 6, 7], we pop 4 (that's x) and 6 (that's y). Both on this level, and nobody earlier said "siblings". So it's True right now. There's no need to go on to 7 or to the next level with 5.

To remember "I saw x on this level" and "I saw y on this level", we keep two flags, found_x and found_y. We reset them to False at the start of each level.

Example 2: x = 2, y = 3 (siblings) → False

When we pop 1, we look at its children: left = 2, right = 3. Both exist, and they are exactly x and y (in either order). So they share the parent 1 → siblings → return False then and there. Why keep looking at other nodes? We already know the answer.

Doubt 1: why check for siblings while standing at the parent, not when we pop x and y themselves?
→ Because the queue only stores nodes, not who their parent was. When we pop 2 later, it has no idea it came from 1. The parent is the only place where both siblings can be seen together. So that is where we catch them.
Doubt 2: why check "left and right both exist" first?
→ To be siblings, a parent must have two children. If one child is missing, they can't be siblings. The check also protects us: reading .val of a None child would crash.
Doubt 3: why check both orders, (x, y) and (y, x)?
→ x might be the left child and y the right child, or the other way round. Both mean "same parent".

Example 3: x = 4, y = 3 (different levels) → False

On the level [2, 3] we pop 3 (that's y) → found_y = True. But x (4) is not on this level, so found_x stays False. At the end of the level, exactly one flag is True. Values are unique, so the other one must be on some other level → they can't be cousins → return False immediately.

Putting it together: what happens at the end of each level

found_xfound_ymeaningaction
TrueTruesame level, and the sibling check never firedreturn True
TrueFalseonly one is here, the other is on another levelreturn False
FalseTrue
FalseFalseneither is on this levelgo to the next level
Doubt 4: could x and y be found on the same level but still be siblings, and we wrongly return True?
→ No. Their parent is on the previous level, and we popped it before this level started. At that moment the sibling check would already have returned False. So if we reach the end of a level with both flags True, they definitely have different parents.

If the loop ever ends without returning (in theory only, since both values exist), we return False.

5Approach steps

  1. Put the root in a queue.
  2. While the queue isn't empty: take the level size, and set found_x = found_y = False.
  3. Pop each node of this level. If its value is x → found_x = True. If it is y → found_y = True.
  4. If it has both children and they are {x, y} in either order → return False (siblings).
  5. Push its existing children.
  6. After the level: both flags True → return True. Exactly one True → return False.
  7. After the loop → return False.

6Code (Python)

Cousins with BFS
from collections import deque

class Solution:
    def isCousins(self, root, x, y):
        queue = deque([root])
        while queue:
            size = len(queue)            # nodes on this level
            found_x = False
            found_y = False
            for _ in range(size):
                node = queue.popleft()
                if node.val == x:
                    found_x = True
                if node.val == y:
                    found_y = True
                # siblings check: both children are x and y
                if node.left and node.right:
                    a = node.left.val
                    b = node.right.val
                    if (a == x and b == y) or (a == y and b == x):
                        return False
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            if found_x and found_y:      # same level, different parents
                return True
            if found_x or found_y:       # only one here, other is elsewhere
                return False
        return False

7Code line by line

linewhat it means
queue = deque([root])Start BFS from the root. No None check needed, since there are at least 2 nodes.
size = len(queue)Freeze the count of nodes on this level, so the for loop handles only them and not the children we add during the loop.
found_x = False found_y = FalseFresh flags for every level. They answer "did I see x / y on this level?"
if node.val == x: found_x = TrueMark x as seen on this level (same for y).
if node.left and node.right:Only a node with two children can be the parent of two siblings. It also makes reading .val safe.
if (a == x and b == y) or (a == y and b == x): return FalseThe two children are exactly x and y → same parent → siblings → False now.
queue.append(...)Normal BFS: queue up the next level.
if found_x and found_y: return TrueBoth on this level, and the sibling check never fired → cousins.
if found_x or found_y: return FalseExactly one was here, so the other is on a different level.
return FalseSafety net. With valid input we always return inside the loop.

8Dry run: watch the queue

Same tree, x = 4, y = 6. Yellow = the nodes of the current level.

level 01pop 1: not x or y. Children 2, 3 are not {4, 6} → push 2, 3. Flags: F, F → next level
level 123pop 2: only a left child → no sibling check, push 4. Pop 3: children 6, 7 are not {4, 6} → push 6, 7. Flags: F, F → next level
level 2467pop 4 → found_x ✓, push 5. Pop 6 → found_y ✓. Pop 7. End of level: both flags True
result5return True. Node 5 is never processed: we stopped at the level where the answer showed up.

Now two more quick runs:

x=6, y=7level 1: pop 2 (push 4), pop 3 → children 6 and 7 are exactly {x, y} → return False (siblings), before level 2 even starts.
x=4, y=3level 1: pop 2, pop 3 → found_y = True, found_x = False → only one flag → return False.

9Complexity, DFS vs BFS & remember

Why the teacher calls BFS better here. On paper, both are O(n) time and O(n) space. But BFS stops at the level where x and y live. Picture a very tall tree where 4 and 6 sit on level 2: BFS answers right after level 2, without touching anything deeper. DFS dives to the bottom of the left side first. So in the best and average cases BFS saves both time and memory. In the worst case they are the same.

Why DFS is still worth knowing. The teacher calls the DFS version more beginner friendly: it just fills four variables and checks one line at the end.

Remember BFS cousinsLevel is free (one round of the for loop = one level). Siblings are caught at the parent → False. End of level: both flags → True, one flag → False.

Part C · Revision page

DFSBFS
how "same level" is checkedpass level + 1 down, compare x level and y level at the endfor free: both popped in the same round of the for loop
how "different parent" is checkedpass parent down, compare x parent and y parentat the parent: if its two children are x and y → siblings → False
extra state4 variables: x/y level, x/y parent2 flags per level: found_x, found_y
stops early?only after both are found (still deep-first)at the level of x and y, or at their parent
base caseneeded (None child stops the recursion)not needed (n ≥ 2)
time / spaceO(n) / O(height), worst O(n)O(n) / O(widest level), worst O(n)
teacher's verdicteasier for beginnersbest approach for this problem
If you remember only 5 lines 1. Cousins = same level + different parent (parent = one step up only).
2. Siblings (same parent) are NOT cousins. Uncle/nephew (different levels) are NOT cousins.
3. DFS: send level + 1 and parent = root to each child, fill 4 boxes.
4. BFS: check siblings at the parent → False. After each level: both flags → True, one flag → False.
5. Unique values let us stop as soon as we find them.
Mistakes to avoid ✗ storing the matching node instead of its parent
✗ starting the root's parent as the root itself (use None)
✗ returning in DFS as soon as x is found (y may still be ahead)
✗ in BFS, forgetting to reset the flags for every level
✗ checking only (left == x and right == y) and forgetting the reverse order
✗ reading node.left.val without checking that the child exists
test it yourself (paste under either solution above)
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right

root = TreeNode(1,
                TreeNode(2, TreeNode(4, None, TreeNode(5))),
                TreeNode(3, TreeNode(6), TreeNode(7)))
s = Solution()
print(s.isCousins(root, 4, 6))   # True
print(s.isCousins(root, 6, 7))   # False
print(s.isCousins(root, 4, 3))   # False
print(s.isCousins(root, 2, 3))   # False
print(s.isCousins(root, 5, 7))   # False

Based on this video: Cousins in Binary Tree | DFS & BFS