Discussion 8: Reconstructing Binary Trees from Traversals
Solutions
Integers.
Write out the pre-order, in-order and post-order traversals of this tree.
Pre-order: 0, 1, 6, 2, 3, 4, 5
In-order: 6, 1, 2, 0, 4, 5, 3
Post-order: 6, 2, 1, 5, 4, 3, 0
Which traversal can we use to determine the root of the tree? How can we determine this?
Now that you’ve identified the tree’s root, can you determine which nodes belong in its left subtree and which nodes belong in its right subtree? Which traversal will be helpful for determining this?
If we perform a pre-order traversal of the nodes in the left subtree, in which order will they be listed? Same question for an in-order traversal.
Using your answer for part (c), describe a recursive procedure that uses sub-ranges of the pre-order and in-order traversal arrays to reconstruct the left and right subtrees?
Use the ideas from the previous parts to draw the tree with the following traversals:
Pre-order: 3, 1, 4, 0, 2, 6, 5
In-order: 1, 4, 3, 2, 0, 5, 6
build() Method
build() helper method is the core of our code to reconstruct a binary tree from its pre-order and in-order traversals. Its purpose is simple yet powerful:
Given a segment of the pre-order array and the corresponding segment of the in-order array, construct the subtree represented by those segments and return its root node.
In essence, build() focuses on one part of the tree at a time — it identifies the current root, splits the arrays into left and right subtrees, and recursively constructs each subtree, ultimately connecting them into the complete binary tree.
|
|
|
|
Understanding the base cases:
- What should the method return if the current pre-order/in-order segments are empty (i.e., there is no subtree left to construct)? What condition will identify this case?
null because there is no subtree to build. This case can be identified when preEnd - preStart == 0 or inEnd - inStart == 0.
- What should the method return if the segment contains exactly one node? What condition will identify this case?
ImmutableBinaryTree containing that single node as the root, with both left and right subtrees set to null. This situation can be identified when preEnd - preStart == 1 or inEnd - inStart == 1.
Finding the subtree segments:
Now, suppose we are not in one of the base cases.
- Write an expression with value equal to the root of the subtree returned by this method.
int rootVal = preorder[preStart];
- How can we use the root to determine the sub-ranges of
inordercorresponding to its left/right subtrees?
rootVal in the inorder array. All elements to the left of this index in inorder belong to the left subtree. All elements to the right of this index in inorder belong to the right subtree.
If rootIndex is the index of rootValue in inorder, then left subtree → inorder[inStart, rootIndex), right subtree → inorder[rootIndex + 1, inEnd).
- How can we determine the sub-ranges of
preordercorresponding to its left/right subtrees?
leftSize = rootIndex - inStart. Using that, we can divide the preorder segment as:
left subtree → preorder[preStart + 1, preStart + leftSize + 1),
right subtree → preorder[preStart + leftSize + 1, preEnd)
Dividing the segments for recursion:
- After determining the correct sub-ranges for the left and right subtrees in both arrays, how can you recursively call
build()to construct each subtree?
build() again on the left and right sub-ranges.
The call for the left subtree uses the indices of the left segment in both arrays, and the call for the right subtree uses the right segment.
For example:left = build(preorder, inorder, preStart + 1, preStart + leftSize + 1, inStart, rootIndex)
right = build(preorder, inorder, preStart + leftSize + 1, preEnd, rootIndex + 1, inEnd)
- How do we use the return values of these recursive calls to build the final tree that we must return?
rootVal, with the left child being the subtree returned from the left call and the right child being the subtree returned from the right call.
TreeReconstruction class
build() method.
|
|
|
|
reconstructTree() method. This definition should consist of a single call to the build() helper method, with appropriately chosen parameters.
|
|
|
|
What are the worst-case time complexity and space complexity of your reconstructTree() method, expressed in terms of \(N=\) preorder.length ?
reconstructTree() is \(O(N^2)\), where \(N\) = preorder.length.
This happens because, for each recursive call, we may need to search for the root value in the inorder array (an \(O(N)\) operation), and there can be up to \(N\) recursive calls in the worst case (for a skewed tree).
The worst-case space complexity is \(O(N)\). This comes from the recursion stack depth, which can be as large as \(N\) in the case of a completely skewed tree.