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

Populating Next Right Pointers

Here every node gets one extra pointer called next, and we have to point it at the node just to its right on the same level. The picture screams "levels", so the teacher solves it first with BFS (level order traversal), which she calls both the easier and the best approach. Then she asks: can DFS do it too? It can, with one clever idea: connect a level while standing one level above it. That idea, "the parent wires up its children", shows up again in many tree problems.

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 · Perfect binary tree & the Node class

Perfect binary tree

A perfect binary tree is the "fullest" kind of tree:

A level is a row of the tree: the root is level 0, its children are level 1, and so on. In a perfect tree, level k has exactly 2k nodes: 1, 2, 4, 8…

This promise is very useful later: if a node has a left child, it surely has a right child too, and if it has no left child, it is a leaf.

The Node class

given by LeetCode, don't write this in the solution
class Node:
    def __init__(self, val=0, left=None, right=None, next=None):
        self.val = val
        self.left = left
        self.right = right
        self.next = next     # starts as None for every node

So each node has four things: a value, a left pointer, a right pointer, and the new next pointer.

Part A · Next pointers with BFS

LeetCode 116

1The question in simple words

You get a perfect binary tree. At the start, every next is None. Make each node's next point to the node right after it on the same level. The last node of each level has nobody to its right, so its next stays None.

before (every next = None)
        1
      /   \
     2     3
    / \   / \
   4   5 6   7
after
        1 → None
      /   \
     2  →  3 → None
    / \   / \
   4 → 5 → 6 → 7 → None

We don't build anything new. We only fill the pointers in place. (LeetCode still wants the root returned at the end, so the code does return root.)

2What the constraints tell us

3Intuition: which pattern does this look like?

So far we know two ways to walk a tree: DFS (go deep) and BFS (go level by level). Look at the "after" picture: the arrows run along each level. That is exactly what level order traversal sees. In BFS, after popping a node, the next node in the queue is its right-hand neighbour on the same level, as long as the current node isn't the last one of the level.

4Building the logic from the example

Which nodes should get a next?

Write the levels out: [1], [2, 3], [4, 5, 6, 7]. In every level, all nodes except the last one point to their neighbour. So we need to know each node's position inside its level.

In level-order BFS we already have it: size = number of nodes in this level, and the for loop counter i goes 0, 1, …, size − 1. So:

For the root: size = 1, i = 0, and size − 1 = 0, so i is not less than 0 → nothing is connected ✓.

Where is the neighbour? Peek, don't pop

After we pop 2 from the queue, the queue's front is 3, its neighbour. So node.next = queue[0]. We only read the front (peek). We must not pop it, because 3 still needs its own turn in the loop: its own next to check, and its children to push.

Doubt 1: why not connect the last node of a level to the queue front too?
→ Because by then the front of the queue already belongs to the next level. After popping 3 (the last of level 1), the queue front is 4. Doing 3.next = 4 would be wrong: 4 is on a different level. That's the whole reason for i < size - 1.
Doubt 2: the children we push go into the same queue. Can they get in the way of the peek?
→ No. Children go to the back. The nodes still waiting from the current level are all in front of them. So while i < size - 1, the front is always a node from the same level.

Then the normal BFS part

After handling next, push the left child and the right child if they exist, exactly like normal level order traversal.

5Approach steps

  1. If root is None → return None.
  2. Put the root in a queue.
  3. While the queue isn't empty: size = len(queue).
  4. For i from 0 to size − 1: pop a node.
  5. If i < size - 1 → node.next = queue[0] (peek).
  6. Push its left and right children if they exist.
  7. Return root.

6Code (Python)

Next right pointers with BFS
from collections import deque

class Solution:
    def connect(self, root):
        if root is None:                 # 0 nodes is allowed
            return None
        queue = deque([root])
        while queue:
            size = len(queue)            # nodes on this level
            for i in range(size):
                node = queue.popleft()
                if i < size - 1:         # not the last node of the level
                    node.next = queue[0] # peek: the neighbour on the right
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        return root

7Code line by line

linewhat it means
if root is None: return NoneBase case for the empty tree (the constraints allow 0 nodes).
queue = deque([root])Start level order from the root.
size = len(queue)How many nodes are on the current level. We need this to know which node is the last one.
for i in range(size):i is the node's position in its level: 0 = leftmost, size − 1 = rightmost.
node = queue.popleft()Take the next node of this level.
if i < size - 1: node.next = queue[0]Not the last node → its right neighbour is at the front of the queue. queue[0] reads it without removing it (a "peek").
queue.append(node.left) / (node.right)Line up the next level, in left-to-right order.
return rootThe tree has been changed in place. LeetCode expects the root back.

8Dry run: watch the queue

Tree 1; 2, 3; 4, 5, 6, 7. Yellow = the node being popped.

start1size = 1
i = 01pop 1. Is 0 < 0? No → last of its level, next stays None. Push 2, 3.
level 123size = 2
i = 02pop 2. 0 < 1 ✓ → peek front = 3 → 2.next = 3. Push 4, 5.
i = 1345pop 3. 1 < 1? No → stays None (the front is 4, from the next level!). Push 6, 7.
level 24567size = 4
i = 04567pop 4. 0 < 3 ✓ → 4.next = 5. No children.
i = 1567pop 5. 1 < 3 ✓ → 5.next = 6.
i = 267pop 6. 2 < 3 ✓ → 6.next = 7.
i = 37pop 7. 3 < 3? No → stays None. Queue empty → done, return root.

9Complexity & remember

The teacher's view: for a beginner this BFS version is both the easiest and the best one. Whenever a problem is about levels, BFS is usually the first thing to try.

Remember BFS next pointersLevel order + position i. If i < size - 1 → node.next = queue[0] (peek, don't pop). The last node of a level stays None.

Part B · Next pointers with DFS

1The question in simple words

Same task: fill every next. But now we walk depth first (recursion), not level by level.

2What the constraints tell us

3Intuition: the problem with DFS, and the fix

DFS goes 1 → 2 → 4 → … When we are standing at 2, the function holds only node 2. We have no way to reach 3 from 2: 3 is not a child of 2. So "standing at a node and connecting it to its neighbour" doesn't work in DFS.

The fix: look one level up. When we stand at 1, we can see both 2 and 3. So let the parent connect its children. In other words, we connect a level while standing on the level above it.

4Building the logic from the example

First, the idea the teacher rejects

One way that works: use DFS to collect the nodes of every level into lists (DFS level order traversal, like we did before: if this level has no list yet, make one, then add the node to it). Then go through each list and link neighbours.

rejected idea: DFS into level lists (works, but wastes space)
class Solution:
    def connect(self, root):
        levels = []                          # levels[k] = nodes of level k
        self.collect(root, 0, levels)
        for row in levels:
            for i in range(len(row) - 1):
                row[i].next = row[i + 1]     # link neighbours in the row
        return root

    def collect(self, node, level, levels):
        if node is None:
            return
        if level == len(levels):             # first node seen on this level
            levels.append([])
        levels[level].append(node)           # store the NODE, not the value
        self.collect(node.left, level + 1, levels)
        self.collect(node.right, level + 1, levels)
Doubt 1: it works, so why reject it?
→ It stores every node in extra lists, which is O(n) extra space, on top of the recursion stack. And it does two passes. The teacher calls it messy: we are really doing BFS's job in a roundabout way. If we want DFS, we should use something DFS is naturally good at.

Rule 1: connect a node's own two children (left → right)

Standing at 1: 1.left.next = 1.right, so 2 → 3. Standing at 2: 2.left.next = 2.right, so 4 → 5. Standing at 3: 6 → 7.

The teacher's side note: even if the right child were missing, this line would only assign None, which is harmless. In a perfect tree it is always there anyway.

Rule 2: connect the right child to the next family's left child

Rule 1 never gives us 5 → 6, because 5 and 6 have different parents. How can we reach 6 while standing at 2?

Look: 2.next is 3. That was already set earlier, when we stood at 1. And 6 is 3.left. So:

2.right.next = 2.next.left → 5.next = 3.left = 6 ✓

That's the trick: the parent's own next pointer is a bridge to the neighbouring family.

When can't we use Rule 2?

When the parent has no next, it is the last node of its level. Example: at 3, 3.next is None, so there is no family to the right, and 7 should stay None. So Rule 2 runs only if root.next exists. Without this check, root.next.left would crash on None.

Doubt 2: we can see that 7 has nobody on its right. Why does the code need a check for it?
→ We see it in the picture. The code only sees pointers. The only way the code can "know" 7 is the last in its row is that its parent 3 has next = None.

The base case: root is None OR root.left is None

Doubt 3: why don't we also check root.right before root.right.next = ...?
→ Because of the perfect tree promise: if the left child exists, the right one exists too. (In a non-perfect tree this code would be unsafe. That's a different LeetCode problem, 117.)

Why the order "connect first, then recurse" matters

Rule 2 at node 2 uses 2.next. That pointer must already be set when we arrive at 2. It is, because 1 set it before calling dfs(2). So we must do the connecting before the recursive calls (preorder: node first, then children). Every node's next is set by its parent before we ever visit it.

5Approach steps

  1. connect(root) calls dfs(root) and returns root.
  2. In dfs: if root is None or root.left is None → return.
  3. root.left.next = root.right (same family).
  4. If root.next exists → root.right.next = root.next.left (next family).
  5. Recurse on root.left, then on root.right.

6Code (Python)

Next right pointers with DFS
class Solution:
    def connect(self, root):
        self.dfs(root)
        return root

    def dfs(self, root):
        if root is None or root.left is None:   # empty, or a leaf
            return
        root.left.next = root.right             # Rule 1: my two children
        if root.next:                           # is there a family to my right?
            root.right.next = root.next.left    # Rule 2: bridge across
        self.dfs(root.left)
        self.dfs(root.right)

7Code line by line

linewhat it means
self.dfs(root) return rootLet the helper fill all the pointers, then hand back the root. Works for the empty tree too (dfs just returns).
if root is None or root.left is None: returnNothing to connect: an empty spot, or a leaf (perfect tree, so no left means no right).
root.left.next = root.rightJoin my own two children.
if root.next:Am I the last node of my level? If I have no next, my right child is the last on its level too.
root.right.next = root.next.leftJoin my right child to my neighbour's left child, using my own next as the bridge.
self.dfs(root.left) self.dfs(root.right)Go down. By now both children have their next set, so they can use it as their bridge.

8Dry run using the call stack

Same tree 1; 2, 3; 4, 5, 6, 7.

  1. dfs(1): left exists. Rule 1: 2.next = 3. 1.next is None → skip Rule 2. Call dfs(2).
  2. dfs(2): Rule 1: 4.next = 5. 2.next = 3 exists → Rule 2: 5.next = 3.left → 5.next = 6. Call dfs(4).
  3. dfs(4): 4.left is None (leaf) → return. Then dfs(5) → leaf → return. dfs(2) is done.
  4. Back in dfs(1), call dfs(3): Rule 1: 6.next = 7. 3.next is None → skip Rule 2, so 7 stays None ✓.
  5. dfs(6), dfs(7): leaves → return. Everything unwinds. All pointers are set.
step 2
dfs(1)dfs(2): 4→5, 5→6
step 3 (deepest)
dfs(1)dfs(2)dfs(4) leaf
step 4
dfs(1)dfs(3): 6→7
pointerset while standing atby rule
2 → 31Rule 1
4 → 52Rule 1
5 → 62 (using 2.next = 3)Rule 2
6 → 73Rule 1
3 → None, 7 → Nonenever touched (their parent / they have no next)

9Complexity, BFS vs DFS & remember

Doubt 4: is the DFS stack really O(n) here?
→ The stack is as tall as the tree. A perfect tree with n nodes has only about log₂ n levels (4095 nodes → 12 levels), so in this problem the stack is actually O(log n), which is smaller than BFS's O(n) queue. O(n) is a safe general upper bound, but here DFS really uses less memory.

BFS vs DFS, in the teacher's words, paraphrased: BFS connects a node while standing on it, because the queue already holds its neighbour. DFS can only hold one node at a time, so it has to connect each level from the level above. Both work, both are O(n) time. For level-type problems, BFS is the more natural first choice.

Side note: LeetCode's follow-up asks for O(1) extra space and says the recursion stack doesn't count. So this DFS version already satisfies it, while the BFS queue does not.

Remember DFS next pointersStand on the parent. left.next = right. If root.next exists: right.next = root.next.left. Connect before recursing. Stop at None or a leaf.

Part C · Revision page

BFSDFS
where we stand when connectingon the node itselfon its parent (one level above)
how we find the neighbourqueue[0] (peek)same family: root.right · next family: root.next.left
how we skip the last nodei < size - 1if root.next (parent has no neighbour)
base caseroot is Noneroot is None or root.left is None
relies on perfect tree?no (works for any tree)yes (left exists ⇒ right exists)
time / spaceO(n) / O(n) (last level ≈ n/2)O(n) / stack = height = O(log n) here
If you remember only 5 lines 1. Each node's next = its right neighbour on the same level. The last of each level stays None.
2. BFS: for position i in a level of size, if i < size - 1 → node.next = queue[0].
3. Peek, don't pop: the neighbour still needs its own turn.
4. DFS: the parent connects its children: left.next = right, and right.next = root.next.left if root.next exists.
5. DFS connects first, then recurses, so each child's next is ready before we visit it.
Mistakes to avoid ✗ connecting the last node of a level to queue[0] (that's the next level)
✗ popping the neighbour instead of peeking
✗ forgetting the empty-tree base case (0 nodes is allowed)
✗ in DFS, using root.next.left without checking root.next
✗ in DFS, recursing before connecting (the child's next isn't ready yet)
✗ using the DFS code on a non-perfect tree (it assumes two children or none)
test it yourself (paste under any solution above)
class Node:
    def __init__(self, val=0, left=None, right=None, next=None):
        self.val, self.left, self.right, self.next = val, left, right, next

root = Node(1, Node(2, Node(4), Node(5)), Node(3, Node(6), Node(7)))
Solution().connect(root)
level = root
while level:                      # walk each level using next
    node, row = level, []
    while node:
        row.append(str(node.val))
        node = node.next
    print("-".join(row))         # 1, then 2-3, then 4-5-6-7
    level = level.left

Based on this video: Populating Next Right Pointers in Each Node | BFS & DFS