← Back to DevBytes

Solving Construct Binary Tree from Preorder and Inorder in Go: Step-by-Step Guide

Introduction to Constructing a Binary Tree from Preorder and Inorder Traversals

Reconstructing a binary tree from its traversal outputs is a classic algorithmic problem that appears frequently in coding interviews and systems that serialize tree-based data structures. Given two arrays — one representing the preorder traversal and the other the inorder traversal of a binary tree — your task is to rebuild the original tree structure. In this tutorial, we will walk through a complete, idiomatic Go implementation, explain the underlying theory, and discuss best practices.

What Is the Problem?

A binary tree can be uniquely reconstructed if you are given both its preorder and inorder traversals, assuming all node values are unique. Preorder visits nodes in the order root → left → right, while inorder visits them in the order left → root → right. By combining these two sequences, you can determine the root, the size of the left subtree, and the size of the right subtree at every step.

Why It Matters

Understanding the Theory

The key insight is that the first element of the preorder array is always the root of the current subtree. Once you know the root, you can locate that same value in the inorder array. Everything to the left of that position in the inorder array belongs to the left subtree, and everything to the right belongs to the right subtree. The number of elements on each side tells you how many nodes to consume from the preorder array for each subtree.

For example, consider:

preorder = [3, 9, 20, 15, 7]
inorder  = [9, 3, 15, 20, 7]

The root is 3 (first in preorder). In the inorder array, 3 sits at index 1, so the left subtree contains one node (9) and the right subtree contains three nodes (15, 20, 7). You then recurse on the corresponding slices of both arrays.

Defining the Tree Structure in Go

Before writing the algorithm, we need a basic binary tree node definition. In Go, this is typically done with a struct:

package main

import "fmt"

// TreeNode represents a node in a binary tree.
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

This struct holds an integer value and pointers to its left and right children. Using pointers allows us to represent empty subtrees with nil.

Implementing the Recursive Solution

The recursive approach is the most intuitive. At each call, we extract the root from the preorder slice, find its index in the inorder slice, and recursively build the left and right subtrees using the appropriate sub-slices.

Basic Recursive Implementation

func buildTree(preorder []int, inorder []int) *TreeNode {
    if len(preorder) == 0 {
        return nil
    }

    // The first element of preorder is the root.
    rootVal := preorder[0]
    root := &TreeNode{Val: rootVal}

    // Find the root's index in inorder.
    rootIndex := 0
    for i, v := range inorder {
        if v == rootVal {
            rootIndex = i
            break
        }
    }

    // Recursively build left and right subtrees.
    root.Left = buildTree(preorder[1:1+rootIndex], inorder[:rootIndex])
    root.Right = buildTree(preorder[1+rootIndex:], inorder[rootIndex+1:])

    return root
}

This implementation is clean and easy to reason about. However, it has two inefficiencies: slicing creates new underlying arrays conceptually (though Go slices share storage), and the linear search for the root index runs in O(n) time at each level, giving an overall O(n²) worst-case time complexity.

Optimizing with a Hash Map

To avoid repeatedly scanning the inorder array for the root index, we can precompute a hash map that maps each value to its index in the inorder array. This reduces lookup time to O(1) and brings the overall time complexity down to O(n).

Optimized Implementation

func buildTreeOptimized(preorder []int, inorder []int) *TreeNode {
    // Build a map from value to index in the inorder array.
    inorderMap := make(map[int]int)
    for i, v := range inorder {
        inorderMap[v] = i
    }

    // Use an index to track the current root in preorder.
    preIdx := 0

    var helper func(left, right int) *TreeNode
    helper = func(left, right int) *TreeNode {
        // If there are no elements to construct the tree.
        if left > right {
            return nil
        }

        // Select the current root from preorder.
        rootVal := preorder[preIdx]
        preIdx++
        root := &TreeNode{Val: rootVal}

        // Split the inorder array at the root's index.
        idx := inorderMap[rootVal]

        // Build left and right subtrees. Order matters: left first!
        root.Left = helper(left, idx-1)
        root.Right = helper(idx+1, right)

        return root
    }

    return helper(0, len(inorder)-1)
}

Notice that we use a closure variable preIdx that increments each time we create a new node. This works because preorder always processes the root before its children, so the next value in preorder is always the root of the next subtree we need to build. The left and right parameters define the current window of the inorder array that belongs to this subtree.

Testing the Implementation

To verify correctness, we can write a helper function that prints the tree using preorder and inorder traversals, then compare the output with the original input arrays.

func preorderTraversal(root *TreeNode) []int {
    if root == nil {
        return nil
    }
    result := []int{root.Val}
    result = append(result, preorderTraversal(root.Left)...)
    result = append(result, preorderTraversal(root.Right)...)
    return result
}

func inorderTraversal(root *TreeNode) []int {
    if root == nil {
        return nil
    }
    result := inorderTraversal(root.Left)
    result = append(result, root.Val)
    result = append(result, inorderTraversal(root.Right)...)
    return result
}

func main() {
    preorder := []int{3, 9, 20, 15, 7}
    inorder := []int{9, 3, 15, 20, 7}

    tree := buildTreeOptimized(preorder, inorder)

    fmt.Println("Reconstructed preorder:", preorderTraversal(tree))
    fmt.Println("Reconstructed inorder: ", inorderTraversal(tree))
}

Running this program should produce:

Reconstructed preorder: [3 9 20 15 7]
Reconstructed inorder:  [9 3 15 20 7]

If the output matches the input arrays, the reconstruction is correct.

Handling Edge Cases

Robust code must handle several edge cases gracefully:

Here is a small validation helper you can add:

func validateInputs(preorder, inorder []int) bool {
    if len(preorder) != len(inorder) {
        return false
    }
    counts := make(map[int]int)
    for _, v := range preorder {
        counts[v]++
    }
    for _, v := range inorder {
        counts[v]--
        if counts[v] < 0 {
            return false
        }
    }
    return true
}

Best Practices

Choose the Optimized Version for Large Inputs

The naive recursive solution is fine for learning and small trees, but for trees with thousands of nodes, the hash map approach is significantly faster. Always prefer O(n) solutions when the problem size can grow.

Avoid Global State

In the optimized implementation, preIdx is captured by the closure. This is safe because the function is not re-entrant across goroutines. If you need concurrency safety, pass the index as a parameter or use a struct to encapsulate state.

Use Meaningful Variable Names

Names like left, right, rootVal, and inorderMap make the code self-documenting. Avoid single-letter variables except for simple loop counters.

Write Table-Driven Tests

Go's testing framework excels at table-driven tests. Create a slice of test cases covering normal trees, edge cases, and skewed trees to ensure your implementation is robust.

func TestBuildTree(t *testing.T) {
    tests := []struct {
        name     string
        preorder []int
        inorder  []int
    }{
        {"empty", []int{}, []int{}},
        {"single node", []int{1}, []int{1}},
        {"balanced", []int{3, 9, 20, 15, 7}, []int{9, 3, 15, 20, 7}},
        {"left skewed", []int{1, 2, 3}, []int{3, 2, 1}},
        {"right skewed", []int{1, 2, 3}, []int{1, 2, 3}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            tree := buildTreeOptimized(tt.preorder, tt.inorder)
            gotPre := preorderTraversal(tree)
            gotIn := inorderTraversal(tree)
            if !slicesEqual(gotPre, tt.preorder) || !slicesEqual(gotIn, tt.inorder) {
                t.Errorf("reconstruction failed for %s", tt.name)
            }
        })
    }
}

func slicesEqual(a, b []int) bool {
    if len(a) != len(b) {
        return false
    }
    for i := range a {
        if a[i] != b[i] {
            return false
        }
    }
    return true
}

Iterative Alternative

For completeness, it is worth knowing that an iterative solution exists using a stack. The idea is to process the preorder array sequentially while using a stack to track nodes whose right subtrees have not yet been built. When the current preorder value matches the next expected inorder value, you pop from the stack and begin building right children. This approach avoids recursion entirely, which can be useful in environments with limited stack space.

func buildTreeIterative(preorder, inorder []int) *TreeNode {
    if len(preorder) == 0 {
        return nil
    }

    root := &TreeNode{Val: preorder[0]}
    stack := []*TreeNode{root}
    inIdx := 0

    for i := 1; i < len(preorder); i++ {
        node := &TreeNode{Val: preorder[i]}
        var parent *TreeNode

        for len(stack) > 0 && stack[len(stack)-1].Val == inorder[inIdx] {
            parent = stack[len(stack)-1]
            stack = stack[:len(stack)-1]
            inIdx++
        }

        if parent != nil {
            parent.Right = node
        } else {
            stack[len(stack)-1].Left = node
        }

        stack = append(stack, node)
    }

    return root
}

This iterative version is more difficult to understand at first glance, but it demonstrates how the problem can be solved without recursion. It runs in O(n) time and uses O(n) space for the stack.

Conclusion

Constructing a binary tree from its preorder and inorder traversals is a foundational problem that deepens your understanding of tree structure, recursion, and algorithm optimization. We started with a straightforward recursive solution, then improved it using a hash map to achieve linear time complexity, and finally explored an iterative alternative for stack-constrained environments. By following the best practices outlined above — validating inputs, writing table-driven tests, and choosing the right approach for your data size — you will be well equipped to implement this algorithm confidently in Go, whether in an interview or a production system.

— Ad —

Google AdSense will appear here after approval

← Back to all articles