官方解答写的非常好,搬运一下
class Solution:
def processChild(self, childNode, prev, leftmost):
if childNode:
# If the "prev" pointer is alread set i.e. if we
# already found atleast one node on the next level,
# setup its next pointer
if prev:
prev.next = childNode
else:
# Else it means this child node is the first node
# we have encountered on the next level, so, we
# set the leftmost pointer
leftmost = childNode
prev = childNode
return prev, leftmost
def connect(self, root: Optional['Node']) -> Optional['Node']:
if not root:
return root
# The root node is the only node on the first level
# and hence its the leftmost node for that level
leftmost = root
# We have no idea about the structure of the tree,
# so, we keep going until we do find the last level.
# The nodes on the last level won't have any children
while leftmost:
# "prev" tracks the latest node on the "next" level
# while "curr" tracks the latest node on the current
# level.
prev, curr = None, leftmost
# We reset this so that we can re-assign it to the leftmost
# node of the next level. Also, if there isn't one, this
# would help break us out of the outermost loop.
leftmost = None
# Iterate on the nodes in the current level using
# the next pointers already established.
while curr:
# Process both the children and update the prev
# and leftmost pointers as necessary.
prev, leftmost = self.processChild(curr.left, prev, leftmost)
prev, leftmost = self.processChild(curr.right, prev, leftmost)
# Move onto the next node.
curr = curr.next
return root
Given a binary tree
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,null,7] Output: [1,#,2,3,#,4,5,7,#] Explanation: Given the above 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 = [] Output: []
Constraints:
- The number of nodes in the tree is in the range
[0, 6000]
. -100 <= Node.val <= 100
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.
Approach 2: Using previously established next pointers
Intuition
We have to process all the nodes of the tree. So we can't reduce the time complexity any further. However, we can try and reduce the space complexity. The reason we need a queue here is because we don't have any idea about the structure of the tree and the kind of branches it has and we need to access all the nodes on a common level, together, and establish connections between them.
Once we are done establishing the next
pointers between the nodes, don't they kind of represent a linked list? After the next
connections are established, all the nodes on a particular level actually form a linked list via these next
pointers. Based on this idea, we have the following intuition for our space efficient algorithm:
We only move on to the level N+1 when we are done establishing the next pointers for the level N. So, since we have access to all the nodes on a particular level via the next pointers, we can use these next pointers to establish the connections for the next level or the level containing their children.
Algorithm
-
We start at the root node. Since there are no more nodes to process on the first level or level
0
, we can establish the next pointers on the next level i.e. level 1. An important thing to remember in this algorithm is that we establish the next pointers for a level NN while we are still on level N−1N−1 and once we are done establishing these new connections, we move on to NN and do the same thing for N+1N+1. -
As we just said, when we go over the nodes of a particular level, their next pointers are already established. This is what helps get rid of the queue data structure from the previous approach and helps save space. To start on a particular level, we just need the
leftmost
node. From there on its just a linked list traversal. -
Based on these ideas, our algorithm will have the following pseudocode:
leftmost = root while (leftmost != null) { curr = leftmost prev = NULL while (curr != null) { → process left child → process right child → set leftmost for the next level curr = curr.next } }
-
Before we proceed with the steps in our algorithm, we need to understand some of the variables we have used above in the pseudocode since they will be important in understanding the implementation.
-
leftmost: represents the corresponding variable on each level. This node is important to discover on each level since this would act as our head of the linked list and we will start our traversal of all the nodes on a level from this node onwards. Since the structure of the tree can be anything, we don't really know what the leftmost node on a level would be. Let's look at a few tree structures and the corresponding leftmost nodes on each level.
-
Oh, in case you are interested in a fun problem that find out all such nodes (rightmost instead of leftmost), check out this problem.
-
curr: As we can see in the pseudocode, this is just the variable we use to traverse all the nodes on the
current
level. It starts off withleftmost
and then follows thenext
pointers all the way to the very end. -
prev: This is the pointer to the
leading
node on thenext
level. We need this pointer because whenever we update the nodecurr
, we assignprev.next
to the left child ofcurr
if one exists, otherwise the right child. When we do so, we also update theprev
pointer. Let's consider an example that highlights how theprev
pointer is updated. Namely, the following example will highlight the 4 possible scenarios for pointer updates:
-
The first case is when the prev
pointer is assigned a non-null value for the very first time i.e. when it is initialized. We start with a null
value and when we find the first node on the next level i.e whenever we find the very first node on the current level that has at least one child, we assign the leftmost child to prev
.
Next is when the node on the current level doesn't have a left child. We then point prev
to the right child of the current node. An important thing to remember in this illustration is that the level 2, 3, 5, 9
already has their next
pointers properly established.
Moving on, we have a node with no children. Here, we don't update the prev
pointer.
And finally, we come across a node with 2 children. We first update prev
to the left child and once the necessary processing is done, we update it to the right child.
Once we are done with the current level, we move on to the next one. One last thing that's left here to update the leftmost
node. We need that node to start traversal on a particular level. Think of it as the head of the linked list. This is easy to do by using the prev
pointer. Whenever we set the value for prev
pointer for the first time corresponding to a level i.e. whenever we set it to it's first node, we also set the head or the leftmost
to that node. So, in the following image, leftmost
originally was 2
and now it would change to 4
.