116. Populating Next Right Pointers In Each Node - Explanation
Description
You are given a perfect binary tree where all leaves are on the same level, and every parent has two children. The binary tree has the following definition:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL.
Initially, all next pointers are set to NULL.
Example 1:
Input: root = [1,2,3,4,5,6,7]
Output: [1,#,2,3,#,4,5,6,7,#]Explanation: Given the above perfect binary tree (Figure A), your function should populate each next pointer to point to its next right node, just like in Figure B. The serialized output is in level order as connected by the next pointers, with '#' signifying the end of each level.
Example 2:
Input: root = [1]
Output: [1,#]Constraints:
0 <= number of nodes in the tree <= 4095-1000 <= Node.val <= 1000
Follow-up:
- You may only use constant extra space.
- The recursive approach is fine. You may assume implicit stack space does not count as extra space for this problem.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Binary Tree Structure - Understanding nodes with left/right children and tree traversal concepts
- Breadth-First Search (BFS) - Level-order traversal using a queue to process nodes level by level
- Depth-First Search (DFS) - Recursive tree traversal and tracking depth during recursion
- Queue Operations - Enqueue, dequeue, and peeking to implement level-order traversal
1. Breadth First Search
Intuition
Level-order traversal naturally groups nodes by their depth, which is exactly what we need to connect nodes on the same level. As we process each level, the next node in the queue is the right neighbor of the current node.
By tracking the size of each level, we can connect all nodes within a level while avoiding connecting the last node of one level to the first node of the next.
Algorithm
- If the
rootisnull, returnnull. - Initialize a queue with the
root. - While the queue is not empty:
- Record the current level size.
- For each node in the current level:
- Dequeue the node.
- If it's not the last node in the level, set its
nextpointer to the front of the queue. - Enqueue its
leftandrightchildren if they exist.
- Return the
root.
"""
# Definition for a Node.
class Node:
def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
self.val = val
self.left = left
self.right = right
self.next = next
"""
class Solution:
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
if not root:
return None
q = deque([root])
while q:
levelSize = len(q)
while levelSize:
node = q.popleft()
if levelSize > 1:
node.next = q[0]
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
levelSize -= 1
return rootTime & Space Complexity
- Time complexity:
- Space complexity:
2. Depth First Search
Intuition
Using dfs, we can traverse the tree and track the rightmost node seen at each depth. When we visit a node, we connect the previous rightmost node at that depth to the current node, then update the rightmost reference.
A hash map stores the most recently visited node at each depth, allowing us to build the next pointers as we traverse left to right.
Algorithm
- Create a hash map to store the rightmost node at each depth.
- Define a
dfsfunction that takes a node and its depth:- If the node is
null, return. - If this depth exists in the map, connect the stored node's
nextpointer to the current node. - Update the map with the current node for this depth.
- Recurse on the
leftchild, then therightchild (both with depth + 1).
- If the node is
- Call
dfsstarting from therootat depth 0. - Return the
root.
"""
# Definition for a Node.
class Node:
def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
self.val = val
self.left = left
self.right = right
self.next = next
"""
class Solution:
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
mp = {}
def dfs(node, depth):
if not node:
return
if depth not in mp:
mp[depth] = node
else:
mp[depth].next = node
mp[depth] = node
dfs(node.left, depth + 1)
dfs(node.right, depth + 1)
dfs(root, 0)
return rootTime & Space Complexity
- Time complexity:
- Space complexity:
3. Depth First Search (Optimal)
Intuition
In a perfect binary tree, every node has either zero or two children, and all leaves are at the same level. This structure lets us establish next pointers without extra space for tracking.
For any node with children, its left child's next is always its right child. And if the node has a next pointer, its right child's next is the left child of the node's next neighbor. This recursive pattern connects the entire tree.
Algorithm
- If the
rootisnull, return it. - If the
roothas aleftchild:- Set the
leftchild'snextto therightchild. - If the
roothas anextpointer, set therightchild'snextto thenextnode'sleftchild. - Recursively connect the
leftsubtree. - Recursively connect the
rightsubtree.
- Set the
- Return the
root.
"""
# Definition for a Node.
class Node:
def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
self.val = val
self.left = left
self.right = right
self.next = next
"""
class Solution:
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
if not root:
return root
if root.left:
root.left.next = root.right
if root.next:
root.right.next = root.next.left
self.connect(root.left)
self.connect(root.right)
return rootTime & Space Complexity
- Time complexity:
- Space complexity: for the recursion stack.
4. Breadth First Search (Optimal)
Intuition
Instead of using a queue, we can leverage the next pointers we've already established to traverse each level. We process the tree level by level, using the current level's next pointers to iterate horizontally while setting up the connections for the next level.
Two pointers track our position: one for the current node being processed, and one for the leftmost node of the next level (so we know where to start the next iteration).
Algorithm
- Initialize
curto therootandnxtto theroot'sleftchild (the start of the next level). - While both
curandnxtare notnull:- Connect
cur.left.nexttocur.right. - If
cur.nextexists, connectcur.right.nexttocur.next.left. - Move
curtocur.next. - If
curbecomesnull, move to the next level: setcurtonxtandnxttocur.left.
- Connect
- Return the
root.
"""
# Definition for a Node.
class Node:
def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
self.val = val
self.left = left
self.right = right
self.next = next
"""
class Solution:
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
cur, nxt = root, root.left if root else None
while cur and nxt:
cur.left.next = cur.right
if cur.next:
cur.right.next = cur.next.left
cur = cur.next
if not cur:
cur = nxt
nxt = cur.left
return rootTime & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Connecting Nodes Across Different Levels
When iterating through nodes, it is easy to accidentally set the last node of one level to point to the first node of the next level. Always track the level size or use a marker to know when a level ends, ensuring the rightmost node's next stays null.
Missing the Cross-Parent Connection
For a node's right child, its next should be the left child of the node's next neighbor (if it exists). Forgetting this connection leaves gaps in the horizontal links, breaking the chain between subtrees rooted at different parents.
Assuming the Optimal Solution Works for Non-Perfect Trees
The O(1) space solution leveraging the perfect binary tree structure (every node has 0 or 2 children, all leaves at same level) does not generalize. Applying it to arbitrary binary trees produces incorrect results because the assumptions about child existence fail.
Sign in to join the discussion