Problem overview
The method Flatten receives the root of a binary tree and must rewire it in place into a "linked list" that reuses the same TreeNode objects. In the result, every node's left pointer is null and its right pointer points to the next node. The nodes must appear in pre-order: a node first, then its whole left subtree, then its whole right subtree.
For the tree [1, 2, 5, 3, 4, null, 6], the pre-order is 1, 2, 3, 4, 5, 6, so the flattened chain is 1, 2, 3, 4, 5, 6 linked through right. An empty tree stays empty, and a single node stays as it is. The method returns nothing: the tree itself is changed.
Approach: build the list from the back
The natural idea is to collect the pre-order into a list and then relink the nodes. That works, but it needs extra memory. The lesson instead relinks while walking, and it does so by visiting the nodes in the reverse of pre-order: right subtree, then left subtree, then the node itself.
Walking backwards lets each node link to something that is already finished. Keep a field, lastNode, that always points to the head of the list built so far. When the walk reaches a node, everything that comes after it in pre-order has already been visited, so lastNode is exactly the node that should follow it. The node sets right to lastNode, clears left, and becomes the new head.
Take the tree above. The reverse order visits 6, 5, 4, 3, 2, 1. Node 6 links to nothing (the list is empty), 5 links to 6, 4 links to 5, 3 links to 4, 2 links to 3 and 1 links to 2. The final chain is 1, 2, 3, 4, 5, 6.
One thing makes this safe. A node's right and left are read only by the recursive calls, and those calls come before the node overwrites them. By the time a node is rewired, both of its subtrees are already finished.
Step by step
- Add a field
lastNode. A field belongs to the object, so every recursive call reads and updates the same variable. A local variable would be separate in each call. - Reset it in
Flatten. Fields keep their values between calls, so a second tree must not start with a stale head from the first one. - Call the helper
Dfson the root. - Declare
Dfsas avoidmethod that takes aTreeNode. It only rewires nodes. - Base case. A
nullsubtree has nothing to flatten, so return. - Recurse right first. The right subtree comes last in pre-order, so it must be linked before anything that points to it.
- Recurse left. After this call,
lastNodeis the first node of the flattened left subtree, which is what the current node must point to. - Link forward. Set
root.right = lastNode. - Clear the left pointer with
root.left = null. The left subtree is already in the chain through therightpointers, so nothing is lost. - Become the head. Set
lastNode = rootso the node processed next will link to this one.
The solution in C#
class Solution {
public void Dfs(TreeNode root) {
if (root == null) {
return;
}
Dfs(root.right);
Dfs(root.left);
root.right = lastNode;
root.left = null;
lastNode = root;
}
TreeNode lastNode;
public void Flatten(TreeNode root) {
lastNode = null;
Dfs(root);
}
};
C# details worth knowing:
- Field order doesn't matter.
DfsuseslastNodebefore the line that declares it. In a C# class, members can refer to each other regardless of where they are written. - Default value. A field of a reference type such as
TreeNodestarts asnull, and without an access modifier it isprivate. The explicit reset inFlattenmatters only when the sameSolutionobject is used more than once. - Reference semantics.
rootis a reference to a node object. Assigning toroot.rightchanges the node itself, which is why the caller sees the flattened tree even thoughFlattenreturnsvoid. Assigning to the parameterrootitself would change only the local copy of the reference. - The trailing semicolon. The
;after the class's closing brace is allowed by C# and does nothing.
Complexity
Time: O(n). Each node is visited once and does a few assignments.
Space: O(h), where h is the height of the tree. No new nodes or lists are created. The only extra memory is the recursion stack, which is O(log n) for a balanced tree and O(n) in the worst case, for example a tree that is a single left-leaning chain.
Edge cases and common mistakes
- Empty tree. A
nullroot returns at the base case and nothing happens. - Single node. Its
rightis set tonull(the reset value oflastNode), itslefttonull, and it stays alone. - Left subtree only. For root 1 with left child 2, node 2 is processed first and then node 1 gets
right = 2andleft = null, giving 1, 2. - Going left first. If the recursion visited left before right,
lastNodewould hold the wrong node when the current node links, and the order would not be pre-order. - Forgetting
root.left = null. The old left pointer would stay and the result would not be a list. - Skipping the reset. A reused
Solutionobject would link the last node of the new tree to the old tree's head. - Using a local variable instead of a field. Each call would then have its own copy, and the nodes could not find the head of the list built so far.
Testing
[1, 2, 5, 3, 4, null, 6]becomes the chain 1, 2, 3, 4, 5, 6.[]stays empty.[0]stays a single node.- A root 1 with only a left child 2 becomes 1, then 2 linked through
right.