Facebook Pixel

117. Populating Next Right Pointers in Each Node II

Problem Description

You are given a binary tree where each node has the following structure:

  • val: an integer value
  • left: pointer to the left child
  • right: pointer to the right child
  • next: pointer that initially points to NULL

Your task is to populate each next pointer so that it points to the node's next right neighbor on the same level. If there is no next right node on the same level, the next pointer should remain NULL.

For example, if you have a binary tree like:

       1
      / \
     2   3
    / \   \
   4   5   7

After connecting the next pointers, the connections would be:

  • Node 1's nextNULL (no node to its right on the same level)
  • Node 2's next → Node 3 (same level, to the right)
  • Node 3's nextNULL (no node to its right on the same level)
  • Node 4's next → Node 5 (same level, to the right)
  • Node 5's next → Node 7 (same level, to the right)
  • Node 7's nextNULL (no node to its right on the same level)

The solution uses a level-order traversal (BFS) approach with a queue. For each level of the tree, it processes all nodes from left to right, connecting each node's next pointer to the subsequent node in the queue at that level. The algorithm maintains a pointer p to track the previous node processed at the current level, allowing it to set p.next = node for each new node encountered. This ensures all nodes at the same level are connected via their next pointers.

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). Each node can have connections to other nodes through left, right, and next pointers.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure where each node has left and right children.

DFS

  • Yes: We arrive at DFS as a potential solution approach for this tree problem.

Conclusion: The flowchart suggests using a DFS (Depth-First Search) approach for connecting the next right pointers in the binary tree.

While the provided solution uses BFS (level-order traversal), DFS is equally valid for this problem. A DFS approach would involve:

  1. Traversing the tree depth-first while keeping track of the level
  2. Maintaining a list or map of the rightmost node seen at each level
  3. As we visit each node, connecting it to the previously seen node at the same level
  4. This can be implemented using either recursive DFS or iterative DFS with a stack

The tree structure naturally lends itself to DFS traversal patterns, making it a suitable choice according to the flowchart's decision path.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight for this problem is recognizing that we need to connect nodes that are at the same level but not necessarily siblings. When we think about tree traversal, we naturally process nodes level by level or depth by depth.

The most straightforward way to connect nodes on the same level is to process them in the order they appear from left to right. This immediately suggests a level-order traversal pattern. If we can visit all nodes at level 1, then all nodes at level 2, and so on, we can easily link them together as we encounter them.

Think of it like people standing in rows for a group photo - we want each person to hold hands with the person to their right in the same row. The natural way to do this is to go row by row (level by level) and have each person extend their hand to the next person we encounter in that row.

When using BFS with a queue, we have a beautiful property: at any point during the traversal, the queue contains all nodes at the current level (and only those nodes) before we start processing them. This means we can:

  1. Know exactly how many nodes are at the current level (len(q))
  2. Process them one by one from left to right
  3. Connect each node to the next one in the queue at that level

The pattern emerges: as we dequeue each node, we can set the previous node's next pointer to point to the current node. By maintaining a p pointer that tracks the "previous" node at the current level, we create a chain of connections across each level. When we finish a level, we reset p to None for the next level.

This approach naturally handles all cases - complete trees, incomplete trees, and even skewed trees - because we're simply connecting whatever nodes exist at each level in their left-to-right order.

Solution Approach

The solution implements a level-order traversal using BFS (Breadth-First Search) with a queue data structure. Here's how the algorithm works:

1. Initialize the Queue

  • Start by checking if the root is None. If it is, return immediately.
  • Create a queue using deque([root]) and add the root node to begin the traversal.

2. Process Each Level

  • The outer while q: loop continues as long as there are nodes to process.
  • At the start of each level, we capture the current queue size with len(q). This tells us exactly how many nodes are at the current level.
  • Initialize a pointer p = None to track the previous node at this level.

3. Connect Nodes Within Each Level

  • The inner for _ in range(len(q)): loop processes exactly the number of nodes at the current level.
  • For each node:
    • Dequeue it with node = q.popleft()
    • If p exists (not the first node in this level), set p.next = node to create the connection
    • Update p = node to make the current node the "previous" for the next iteration
    • Add the node's children to the queue for the next level: first node.left, then node.right (if they exist)

4. Level Completion

  • After processing all nodes at a level, the last node's next automatically remains NULL (its initial value).
  • The algorithm moves to the next level with a fresh p = None.

Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once.

Space Complexity: O(w) where w is the maximum width of the tree. In the worst case (a complete binary tree), the last level could contain up to n/2 nodes, making it O(n).

The beauty of this approach is that the queue naturally maintains the left-to-right order of nodes at each level, and by processing exactly len(q) nodes at a time, we ensure we only connect nodes within the same level.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach. Consider this binary tree:

     1
    / \
   2   3
  /     \
 4       5

Initial State:

  • All next pointers are NULL
  • Queue: [1]

Level 1 Processing (1 node):

  • len(q) = 1, so we'll process 1 node
  • p = None (start of level)
  • Dequeue node 1:
    • Since p = None, we don't set any next pointer
    • Update p = node 1
    • Add children: Queue becomes [2, 3]
  • Result: Node 1's next remains NULL

Level 2 Processing (2 nodes):

  • len(q) = 2, so we'll process 2 nodes
  • p = None (start of new level)
  • Iteration 1 - Dequeue node 2:
    • Since p = None, we don't set any next pointer
    • Update p = node 2
    • Add children: Queue becomes [3, 4]
  • Iteration 2 - Dequeue node 3:
    • Since p = node 2, set node 2.next = node 3
    • Update p = node 3
    • Add children: Queue becomes [4, 5]
  • Result: Node 2 → Node 3, Node 3's next remains NULL

Level 3 Processing (2 nodes):

  • len(q) = 2, so we'll process 2 nodes
  • p = None (start of new level)
  • Iteration 1 - Dequeue node 4:
    • Since p = None, we don't set any next pointer
    • Update p = node 4
    • No children to add
  • Iteration 2 - Dequeue node 5:
    • Since p = node 4, set node 4.next = node 5
    • Update p = node 5
    • No children to add
  • Result: Node 4 → Node 5, Node 5's next remains NULL

Final connections:

  • Level 1: 1 → NULL
  • Level 2: 2 → 3 → NULL
  • Level 3: 4 → 5 → NULL

The key insight is that by processing exactly len(q) nodes at each level and maintaining the previous node pointer p, we naturally create the left-to-right connections within each level while keeping levels separate.

Solution Implementation

1"""
2# Definition for a Node.
3class Node:
4    def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
5        self.val = val
6        self.left = left
7        self.right = right
8        self.next = next
9"""
10
11from collections import deque
12from typing import Optional
13
14
15class Solution:
16    def connect(self, root: Optional['Node']) -> Optional['Node']:
17        """
18        Connects each node to its next right node in the same level.
19        Uses BFS (level-order traversal) to process nodes level by level.
20      
21        Args:
22            root: The root of the binary tree
23          
24        Returns:
25            The root of the modified tree with next pointers set
26        """
27        # Handle empty tree case
28        if root is None:
29            return root
30      
31        # Initialize queue with root node for BFS
32        queue = deque([root])
33      
34        # Process tree level by level
35        while queue:
36            # Store the previous node in the current level
37            previous_node = None
38            # Get the number of nodes in the current level
39            level_size = len(queue)
40          
41            # Process all nodes in the current level
42            for _ in range(level_size):
43                # Get the next node from the queue
44                current_node = queue.popleft()
45              
46                # Connect previous node to current node
47                if previous_node:
48                    previous_node.next = current_node
49              
50                # Update previous node for next iteration
51                previous_node = current_node
52              
53                # Add children to queue for next level processing
54                if current_node.left:
55                    queue.append(current_node.left)
56                if current_node.right:
57                    queue.append(current_node.right)
58      
59        return root
60
1/*
2// Definition for a Node.
3class Node {
4    public int val;
5    public Node left;
6    public Node right;
7    public Node next;
8
9    public Node() {}
10
11    public Node(int _val) {
12        val = _val;
13    }
14
15    public Node(int _val, Node _left, Node _right, Node _next) {
16        val = _val;
17        left = _left;
18        right = _right;
19        next = _next;
20    }
21};
22*/
23
24class Solution {
25    /**
26     * Connects each node to its next right node in the same level.
27     * Uses BFS (level-order traversal) to process nodes level by level.
28     * 
29     * @param root The root of the binary tree
30     * @return The root of the modified tree with next pointers set
31     */
32    public Node connect(Node root) {
33        // Handle empty tree case
34        if (root == null) {
35            return root;
36        }
37      
38        // Initialize queue for BFS traversal
39        Deque<Node> queue = new ArrayDeque<>();
40        queue.offer(root);
41      
42        // Process tree level by level
43        while (!queue.isEmpty()) {
44            // Track the previous node in the current level
45            Node previousNode = null;
46          
47            // Get the number of nodes in the current level
48            int levelSize = queue.size();
49          
50            // Process all nodes in the current level
51            for (int i = 0; i < levelSize; i++) {
52                // Dequeue the current node
53                Node currentNode = queue.poll();
54              
55                // Connect previous node to current node if previous exists
56                if (previousNode != null) {
57                    previousNode.next = currentNode;
58                }
59              
60                // Update previous node for next iteration
61                previousNode = currentNode;
62              
63                // Add left child to queue if it exists
64                if (currentNode.left != null) {
65                    queue.offer(currentNode.left);
66                }
67              
68                // Add right child to queue if it exists
69                if (currentNode.right != null) {
70                    queue.offer(currentNode.right);
71                }
72            }
73        }
74      
75        return root;
76    }
77}
78
1/*
2// Definition for a Node.
3class Node {
4public:
5    int val;
6    Node* left;
7    Node* right;
8    Node* next;
9
10    Node() : val(0), left(NULL), right(NULL), next(NULL) {}
11
12    Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
13
14    Node(int _val, Node* _left, Node* _right, Node* _next)
15        : val(_val), left(_left), right(_right), next(_next) {}
16};
17*/
18
19class Solution {
20public:
21    Node* connect(Node* root) {
22        // Handle empty tree case
23        if (!root) {
24            return root;
25        }
26      
27        // Initialize queue for level-order traversal
28        queue<Node*> nodeQueue;
29        nodeQueue.push(root);
30      
31        // Process tree level by level
32        while (!nodeQueue.empty()) {
33            // Track the previous node in the current level
34            Node* previousNode = nullptr;
35          
36            // Get the number of nodes in the current level
37            int levelSize = nodeQueue.size();
38          
39            // Process all nodes in the current level
40            for (int i = 0; i < levelSize; ++i) {
41                // Get the current node from the queue
42                Node* currentNode = nodeQueue.front();
43                nodeQueue.pop();
44              
45                // Connect the previous node to the current node
46                if (previousNode) {
47                    previousNode->next = currentNode;
48                }
49              
50                // Update the previous node for the next iteration
51                previousNode = currentNode;
52              
53                // Add the left child to the queue if it exists
54                if (currentNode->left) {
55                    nodeQueue.push(currentNode->left);
56                }
57              
58                // Add the right child to the queue if it exists
59                if (currentNode->right) {
60                    nodeQueue.push(currentNode->right);
61                }
62            }
63            // The last node in each level automatically has next = nullptr
64        }
65      
66        return root;
67    }
68};
69
1/**
2 * Definition for Node.
3 * class Node {
4 *     val: number
5 *     left: Node | null
6 *     right: Node | null
7 *     next: Node | null
8 *     constructor(val?: number, left?: Node, right?: Node, next?: Node) {
9 *         this.val = (val===undefined ? 0 : val)
10 *         this.left = (left===undefined ? null : left)
11 *         this.right = (right===undefined ? null : right)
12 *         this.next = (next===undefined ? null : next)
13 *     }
14 * }
15 */
16
17/**
18 * Connects each node to its next right node in the same level of a binary tree.
19 * Uses level-order traversal (BFS) to process nodes level by level.
20 * 
21 * @param root - The root node of the binary tree
22 * @returns The root of the modified tree with next pointers set
23 */
24function connect(root: Node | null): Node | null {
25    // Handle empty tree case
26    if (!root) {
27        return null;
28    }
29  
30    // Initialize queue with root node for BFS traversal
31    const currentLevelQueue: Node[] = [root];
32  
33    // Process tree level by level
34    while (currentLevelQueue.length > 0) {
35        // Queue to store nodes of the next level
36        const nextLevelQueue: Node[] = [];
37      
38        // Previous node in the current level for linking
39        let previousNode: Node | null = null;
40      
41        // Process all nodes in the current level
42        for (const currentNode of currentLevelQueue) {
43            // Link previous node to current node if exists
44            if (previousNode) {
45                previousNode.next = currentNode;
46            }
47          
48            // Update previous node reference
49            previousNode = currentNode;
50          
51            // Add children to next level queue if they exist
52            const { left, right } = currentNode;
53            if (left) {
54                nextLevelQueue.push(left);
55            }
56            if (right) {
57                nextLevelQueue.push(right);
58            }
59        }
60      
61        // Replace current level queue with next level queue
62        currentLevelQueue.splice(0, currentLevelQueue.length, ...nextLevelQueue);
63    }
64  
65    return root;
66}
67

Time and Space Complexity

Time Complexity: O(n)

The algorithm uses BFS (Breadth-First Search) to traverse the binary tree level by level. Each node in the tree is visited exactly once - it's added to the queue once and processed once when it's dequeued. For each node, we perform constant time operations: dequeuing, setting the next pointer, and enqueuing its children. Since we visit all n nodes exactly once and perform O(1) operations for each node, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity is determined by the maximum size of the queue q during execution. In the worst case, the queue needs to store all nodes at the widest level of the tree. For a complete binary tree, the maximum width occurs at the last level, which can contain up to n/2 nodes (approximately half of all nodes in a complete binary tree are at the last level). Therefore, the space complexity is O(n/2) = O(n). Additionally, we use a constant amount of extra space for variables like p and node, but this doesn't affect the overall space complexity.

Common Pitfalls

1. Not Maintaining Level Boundaries

One of the most common mistakes is failing to properly track where one level ends and the next begins. Without capturing level_size = len(queue) at the start of each level, you might accidentally connect nodes from different levels.

Incorrect approach:

# WRONG: This connects nodes across different levels
while queue:
    node = queue.popleft()
    if queue:  # This might be a node from the next level!
        node.next = queue[0]
    if node.left:
        queue.append(node.left)
    if node.right:
        queue.append(node.right)

Solution: Always capture the queue size before processing each level and iterate exactly that many times.

2. Forgetting to Reset the Previous Pointer

Another pitfall is not resetting the previous_node pointer to None at the start of each new level. This could cause the last node of one level to incorrectly point to the first node of the next level.

Incorrect approach:

# WRONG: previous_node not reset between levels
previous_node = None
while queue:
    level_size = len(queue)
    # Missing: previous_node = None here!
    for _ in range(level_size):
        current_node = queue.popleft()
        if previous_node:
            previous_node.next = current_node
        previous_node = current_node
        # ... add children

Solution: Initialize previous_node = None inside the outer while loop, right after determining the level size.

3. Using the Wrong Queue Operations

Using pop() instead of popleft() would process nodes in LIFO order rather than FIFO, breaking the left-to-right traversal requirement.

Incorrect approach:

# WRONG: Using pop() gives LIFO behavior
current_node = queue.pop()  # This gets the last element!

Solution: Always use popleft() for BFS to maintain FIFO order and ensure left-to-right processing.

4. Modifying Queue Size During Level Processing

Some might try to optimize by checking while queue and level_size > 0: but if you're not careful with the counter, you might process nodes from multiple levels together.

Solution: Use a simple for _ in range(level_size): loop which guarantees exactly level_size iterations, making the logic cleaner and less error-prone.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More