Discussion 8: Reconstructing Binary Trees from Traversals

Download Handout download

In lecture, we discussed three different traversal strategies for binary trees: pre-order, in-order and post-order traversals. These traversals provide different ways to “visit” all nodes in a tree and are fundamental for many algorithms, including expression evaluation, tree copying, and serialization. In this discussion, we’ll explore a challenging and interesting problem: Given the pre-order and in-order traversals of a binary tree, reconstruct the original tree.

Learning Outcomes

Reminder: Discussion Guidelines

The work that you complete in discussion serves as a formative assessment tool, meaning it offers the opportunity to assess your understanding of the material and for our course staff to get a “pulse” on how things are going so we can make adjustments in future classes. Your grade in discussion is based entirely on attendance and participation; if you show up and you are actively engaged with the activity (working on the activity on your computer, discussing the activity with your group, asking and answering questions, etc.) for the entire 50-minute section period, you will earn full credit. If you complete the activity early, helping other students is a great way to further your own understanding. You do not need to submit any of the work that you complete during discussion.

Since discussion activities are not graded for correctness, we do not place any restrictions on resources that you may use to complete them, which include notes, books, unrestricted conversations with other students, internet searches, and the use of large language models or other generative AI. We advise you to be pragmatic about your use of these resources and think critically about whether they are enhancing your learning. Discussion activities are intended to serve as “strength training” for programming tasks we will expect on assignments and exams (and that you will encounter in future courses and careers), and their main benefit comes from critically thinking to “puzzle” them out.

Working together in small groups is encouraged during discussion!

Exercise 1: Converting Between Tree Structure and Traversals
Suppose we have the following binary tree whose nodes are labeled with Integers.
(a)

Write out the pre-order, in-order and post-order traversals of this tree.

Pre-order:

In-order:

Post-order:

Now, let's imagine doing this process in reverse. Suppose that you were given only the pre-order and in-order traversals of this tree. Let's think about how we can use this information to re-construct the picture of the tree.
(b)
Which traversal can we use to determine the root of the tree? How can we determine this?
(c)
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?
(d)
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.
(e)
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?
(f)

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

Exercise 2: Planning the build() Method
The 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.
1
2
3
4
5
6
/**
 * Constructs the subtree described by pre-order traversal segment `preorder[preStart,preEnd)`
 * and in-order traversal segment `inorder[inStart,inEnd)` and returns a reference to its root.
 */
private static ImmutableBinaryTree<Integer> build(int[] preorder, int[] inorder, 
    int preStart, int preEnd, int inStart, int inEnd) { ... } 
1
2
3
4
5
6
/**
 * Constructs the subtree described by pre-order traversal segment `preorder[preStart,preEnd)`
 * and in-order traversal segment `inorder[inStart,inEnd)` and returns a reference to its root.
 */
private static ImmutableBinaryTree<Integer> build(int[] preorder, int[] inorder, 
    int preStart, int preEnd, int inStart, int inEnd) { ... } 
(a)

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?
  • What should the method return if the segment contains exactly one node? What condition will identify this case?
(b)

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.
  • How can we use the root to determine the sub-ranges of inorder corresponding to its left/right subtrees?
  • How can we determine the sub-ranges of preorder corresponding to its left/right subtrees?
(c)

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?
  • How do we use the return values of these recursive calls to build the final tree that we must return?
Exercise 3: The TreeReconstruction class
Download and look through the starter code. The BinaryTree and ImmutableBinaryTree classes are from the lecture code. You will only need to modify the TreeReconstruction class.
(a)
Use your ideas from the previous exercise to complete the definition of the build() method.
Now that you’ve implemented the recursive helper method build(), it’s time to complete the top-level method, the one that clients will actually call. This is the method that reconstructs the entire binary tree from its traversal arrays:
1
2
3
4
5
6
/**
 * Reconstructs a binary tree from its pre-order and in-order traversals.
 * Requires that `preorder` and `inorder` are valid pre-order and in-order 
 * traversals (respectively) of the same non-empty binary tree.
 */
public static ImmutableBinaryTree<Integer> reconstructTree(int[] preorder, int[] inorder) { ... } 
1
2
3
4
5
6
/**
 * Reconstructs a binary tree from its pre-order and in-order traversals.
 * Requires that `preorder` and `inorder` are valid pre-order and in-order 
 * traversals (respectively) of the same non-empty binary tree.
 */
public static ImmutableBinaryTree<Integer> reconstructTree(int[] preorder, int[] inorder) { ... } 
(b)
Complete the definition of the reconstructTree() method. This definition should consist of a single call to the build() helper method, with appropriately chosen parameters.
Use the provided unit tests to check the correctness of your implementation.
(c)
What are the worst-case time complexity and space complexity of your reconstructTree() method, expressed in terms of \(N=\) preorder.length ?
Exercise 4: (Bonus Challenge) Considering Other Traversals
Up to this point, you’ve seen how combining pre-order and in-order traversals allows you to uniquely reconstruct a binary tree. What if we try using the pre-order and post-order traversals instead?
Give an example of two different binary trees that have the same pre-order and post-order traversals. This shows that the in-order traversal is necessary to uniquely reconstruct a binary tree.