Problem overview
The function flatten receives the root of a binary tree, a TreeNode?, and returns nothing. It must rewire the tree in place so that it becomes a "linked list" made of the same TreeNode objects: every left pointer is null, every right pointer points to the next node, and the nodes appear in preorder (node, then left subtree, then right subtree).
For the tree with root 1, children 2 and 5, node 2 having children 3 and 4, and node 5 having the right child 6, preorder is 1, 2, 3, 4, 5, 6. After flattening, node 1's right pointer leads to 2, then 3, 4, 5 and 6, and no node has a left child. An empty tree stays empty, and a single node stays as it is.
Approach: build the list from the back
The obvious plan is to flatten the left subtree, flatten the right subtree, and then splice the right list onto the end of the left one. That needs a walk to find the end of the left list.
A simpler plan processes the nodes in reverse preorder: right subtree first, then left subtree, then the node. Preorder is node, left, right, so the reverse is right, left, node. When a node is finally handled, everything that follows it in the final list has already been flattened. A variable lastNode holds the head of that finished part. The node only has to point its right at lastNode, clear its left, and become the new lastNode.
Trace it on a root 1 with left child 2 and right child 3. The recursion handles 3 first: its right becomes null (nothing was flattened yet) and lastNode is 3. Then 2: its right becomes 3, and lastNode is 2. Last, 1: its right becomes 2 and its left is cleared. The list is 1, 2, 3.
Step by step
- Declare a property
lastNodeof typeTreeNode?, starting atnull. It lives on theSolutionobject, so every recursive call reads and updates the same one. - Reset it in
flatten. ASolutionobject may be used for several trees, and the property keeps its value between calls. Without the reset, the last node of a new list would point into the previous tree. - Call
dfs(root)to flatten the entire tree. - Declare
dfswith aTreeNode?parameter. It returns nothing, because it rewires nodes in place. - Return on a null root. An empty subtree contributes nothing.
- Recurse on
root.rightfirst. The whole right side is flattened and recorded inlastNodebefore this node is touched. - Recurse on
root.leftnext. The last node of the left list links to the head of the right list, and the head of the left list is what ends up directly afterroot. - Set
root.right = lastNode, linking this node to the list built so far. - Set
root.left = null. The left subtree is already part of the list throughrightpointers, so nothing is lost. - Set
lastNode = root. This node is now the head of the flattened part, and the node handled next links to it.
The solution in Kotlin
class Solution {
fun dfs(root: TreeNode?) {
if (root == null) {
return
}
dfs(root.right)
dfs(root.left)
root.right = lastNode
root.left = null
lastNode = root
}
var lastNode: TreeNode? = null
fun flatten(root: TreeNode?) {
lastNode = null
dfs(root)
}
}
The Kotlin details worth a closer look:
- A property declared after its use.
dfsuseslastNodeeven though the property is declared below it. Inside a class, members can refer to each other in any order. - Properties keep their value.
var lastNode: TreeNode? = nullis initialised once, when theSolutionobject is created. That is whyflattensets it tonullitself. - The type must be nullable. The first node to be linked points to nothing, so
lastNodeneedsTreeNode?. This also matchesroot.right, which is aTreeNode?. - Smart cast. After the
root == nullcheck,rootis treated as a non-nullTreeNode, soroot.rightandroot.leftneed no?.. Assigning to them is allowed because the lesson'sTreeNodedeclares them asvar. - Order of the two recursive calls. Both subtrees are processed before the node's pointers change, so
dfs(root.left)still sees the original left subtree.
Complexity
Time: O(n) for n nodes. Each node is processed once, with a constant number of assignments.
Space: O(h) for the recursion, where h is the height of the tree. A balanced tree has h near log n, while a tree shaped like a chain has h = n. No new nodes are created, so the extra memory is only the call stack and one reference.
Edge cases and common mistakes
- Empty tree.
flatten(null)resetslastNode, returns at once and changes nothing. - Single node. A root of 0 gets
right = nullandleft = null, so it is unchanged. - Forgetting to clear
left. The list would still contain the old left pointers, and the tree would not be flat. - Skipping the reset of
lastNode. Reusing aSolutionfor a second tree would link that tree's final node to the previous list. - Recursing left first. With this "build from the back" technique the left list would be linked first and end up after the right one, breaking preorder.
- Returning something. The task changes the tree in place, and
flattenreturns nothing.
Testing
- The tree 1 (left 2 with children 3 and 4, right 5 with right child 6) flattens to the chain 1, 2, 3, 4, 5, 6.
- Root 1 with children 2 and 3 flattens to 1, 2, 3.
- A single node 0 stays 0.
nullstaysnull.