Facebook Pixel

889. Construct Binary Tree from Preorder and Postorder Traversal

Problem Description

You are given two integer arrays representing tree traversals:

  • preorder: the preorder traversal of a binary tree with distinct values
  • postorder: the postorder traversal of the same binary tree

Your task is to reconstruct and return the original binary tree from these two traversals.

Key Points:

  • All values in the tree are distinct (no duplicates)
  • Both arrays represent the same binary tree
  • If multiple valid trees can be constructed from the given traversals, you may return any one of them

Understanding the Traversals:

  • Preorder traversal visits nodes in this order: root → left subtree → right subtree
  • Postorder traversal visits nodes in this order: left subtree → right subtree → root

For example, if you have a tree:

      1
     / \
    2   3
  • Preorder would be: [1, 2, 3]
  • Postorder would be: [2, 3, 1]

The challenge is to use the properties of these two traversal orders to uniquely identify the structure of the tree and reconstruct it. Since the root appears first in preorder and last in postorder, you can identify the root. The subtrees can then be determined by finding where the left subtree's root (second element in preorder) appears in the postorder array, which helps partition the arrays into left and right subtree segments.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Let's think about what we know from the two traversal patterns:

In preorder, the first element is always the root. The pattern is [root, ...left subtree..., ...right subtree...].

In postorder, the last element is always the root. The pattern is [...left subtree..., ...right subtree..., root].

This immediately tells us that preorder[0] is our root node, and it matches postorder[-1].

Now comes the key insight: How do we find where the left subtree ends and the right subtree begins?

If the tree has a left subtree, then preorder[1] must be the root of the left subtree (since preorder visits root → left → right). We can find this value in the postorder array. Here's why this is useful:

In postorder, the root of the left subtree appears at the end of the left subtree section. So if we find preorder[1] in postorder at index i, then:

  • Elements from postorder[0] to postorder[i] form the left subtree
  • Elements from postorder[i+1] to postorder[-2] form the right subtree (excluding the main root at postorder[-1])

Once we know the size of the left subtree (let's call it m), we can partition both arrays:

  • In preorder: left subtree is preorder[1:1+m], right subtree is preorder[1+m:]
  • In postorder: left subtree is postorder[0:m], right subtree is postorder[m:-1]

This naturally leads to a recursive solution: we can apply the same logic to each subtree, treating each subtree's portion of the arrays as a smaller version of the original problem.

The base cases are straightforward:

  • If the array is empty, return None
  • If the array has one element, return a leaf node

To optimize lookups, we can precompute a hashmap that stores each value's position in the postorder array, allowing us to find the left subtree root's position in O(1) time instead of O(n).

Learn more about Tree, Divide and Conquer and Binary Tree patterns.

Solution Approach

The solution uses a recursive approach with a helper function dfs(a, b, c, d) where:

  • [a, b] represents the current range in the preorder array
  • [c, d] represents the current range in the postorder array

Step 1: Create a Position HashMap

First, we build a hashmap pos that stores each value's index in the postorder array:

pos = {x: i for i, x in enumerate(postorder)}

This allows O(1) lookups when we need to find where a value appears in postorder.

Step 2: Implement the Recursive Function

The dfs function works as follows:

  1. Base Case - Empty Range: If a > b, the range is empty, return None.

  2. Create Root Node: The root is always at preorder[a]:

    root = TreeNode(preorder[a])
  3. Base Case - Single Node: If a == b, this is a leaf node with no children, return the root directly.

  4. Find Left Subtree Boundary:

    • The left subtree's root (if it exists) is preorder[a + 1]
    • Find its position in postorder: i = pos[preorder[a + 1]]
    • Calculate the number of nodes in left subtree: m = i - c + 1

    This works because in postorder, all nodes of the left subtree appear before its root at position i.

  5. Recursively Build Subtrees:

    • Left subtree:
      • Preorder range: [a + 1, a + m] (skip root, take next m elements)
      • Postorder range: [c, i] (from start to left subtree root)
    • Right subtree:
      • Preorder range: [a + m + 1, b] (skip root and left subtree)
      • Postorder range: [i + 1, d - 1] (after left subtree, exclude main root)
    root.left = dfs(a + 1, a + m, c, i)
    root.right = dfs(a + m + 1, b, i + 1, d - 1)
  6. Return the constructed tree: Return the root with its attached subtrees.

Step 3: Initial Call

Start the recursion with the full range of both arrays:

return dfs(0, len(preorder) - 1, 0, len(postorder) - 1)

Time Complexity: O(n) where n is the number of nodes, as each node is processed once.

Space Complexity: O(n) for the hashmap and the recursion stack in the worst case (skewed tree).

The elegance of this solution lies in using the properties of preorder and postorder traversals to identify subtree boundaries without ambiguity, leveraging the fact that all values are distinct.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with a concrete example to see how it reconstructs the tree.

Given:

  • preorder = [1, 2, 4, 5, 3, 6]
  • postorder = [4, 5, 2, 6, 3, 1]

Step 1: Build the position hashmap

pos = {4: 0, 5: 1, 2: 2, 6: 3, 3: 4, 1: 5}

Step 2: Initial call to dfs(0, 5, 0, 5)

First recursive call (entire tree):

  • Range: preorder[0:5], postorder[0:5]
  • Root = preorder[0] = 1
  • Left subtree root candidate = preorder[1] = 2
  • Find 2 in postorder: pos[2] = 2
  • Left subtree size m = 2 - 0 + 1 = 3
  • Split:
    • Left subtree: dfs(1, 3, 0, 2) → preorder[1:3]=[2,4,5], postorder[0:2]=[4,5,2]
    • Right subtree: dfs(4, 5, 3, 4) → preorder[4:5]=[3,6], postorder[3:4]=[6,3]

Second recursive call (left subtree of 1):

  • Range: preorder[1:3], postorder[0:2]
  • Root = preorder[1] = 2
  • Left subtree root candidate = preorder[2] = 4
  • Find 4 in postorder: pos[4] = 0
  • Left subtree size m = 0 - 0 + 1 = 1
  • Split:
    • Left subtree: dfs(2, 2, 0, 0) → preorder[2:2]=[4], postorder[0:0]=[4]
    • Right subtree: dfs(3, 3, 1, 1) → preorder[3:3]=[5], postorder[1:1]=[5]

Third recursive call (node 4):

  • Single element, return TreeNode(4)

Fourth recursive call (node 5):

  • Single element, return TreeNode(5)

Node 2 now has left child 4 and right child 5.

Fifth recursive call (right subtree of 1):

  • Range: preorder[4:5], postorder[3:4]
  • Root = preorder[4] = 3
  • Left subtree root candidate = preorder[5] = 6
  • Find 6 in postorder: pos[6] = 3
  • Left subtree size m = 3 - 3 + 1 = 1
  • Split:
    • Left subtree: dfs(5, 5, 3, 3) → preorder[5:5]=[6], postorder[3:3]=[6]
    • Right subtree: dfs(6, 5, 4, 3) → empty (since 6 > 5)

Sixth recursive call (node 6):

  • Single element, return TreeNode(6)

Seventh recursive call (empty right subtree of 3):

  • a > b, return None

Node 3 now has left child 6 and no right child.

Final Tree Structure:

      1
     / \
    2   3
   / \  /
  4  5 6

This matches our original traversals:

  • Preorder (root→left→right): 1 → 2 → 4 → 5 → 3 → 6 ✓
  • Postorder (left→right→root): 4 → 5 → 2 → 6 → 3 → 1 ✓

The key insight demonstrated here is how finding the left subtree root in postorder tells us exactly how many nodes belong to the left subtree, allowing us to correctly partition both arrays for recursive processing.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from typing import List, Optional
9
10class Solution:
11    def constructFromPrePost(
12        self, preorder: List[int], postorder: List[int]
13    ) -> Optional[TreeNode]:
14        """
15        Construct a binary tree from preorder and postorder traversal arrays.
16      
17        Args:
18            preorder: List of node values in preorder traversal
19            postorder: List of node values in postorder traversal
20          
21        Returns:
22            Root node of the constructed binary tree
23        """
24      
25        def build_tree(
26            pre_start: int, 
27            pre_end: int, 
28            post_start: int, 
29            post_end: int
30        ) -> Optional[TreeNode]:
31            """
32            Recursively build tree from given ranges of preorder and postorder arrays.
33          
34            Args:
35                pre_start: Starting index in preorder array
36                pre_end: Ending index in preorder array
37                post_start: Starting index in postorder array
38                post_end: Ending index in postorder array
39              
40            Returns:
41                Root node of the subtree
42            """
43            # Base case: invalid range
44            if pre_start > pre_end:
45                return None
46          
47            # Create root node with first element in preorder range
48            root = TreeNode(preorder[pre_start])
49          
50            # Base case: single node (leaf)
51            if pre_start == pre_end:
52                return root
53          
54            # Find the position of left subtree root in postorder
55            # The second element in preorder is the left child root
56            left_root_val = preorder[pre_start + 1]
57            left_root_post_idx = postorder_index_map[left_root_val]
58          
59            # Calculate the size of left subtree
60            # Elements from post_start to left_root_post_idx form the left subtree
61            left_subtree_size = left_root_post_idx - post_start + 1
62          
63            # Recursively build left subtree
64            root.left = build_tree(
65                pre_start + 1,                          # Left subtree starts after root in preorder
66                pre_start + left_subtree_size,          # Left subtree ends based on its size
67                post_start,                             # Left subtree starts at beginning in postorder
68                left_root_post_idx                      # Left subtree ends at its root in postorder
69            )
70          
71            # Recursively build right subtree
72            root.right = build_tree(
73                pre_start + left_subtree_size + 1,      # Right subtree starts after left subtree in preorder
74                pre_end,                                # Right subtree ends at the end in preorder
75                left_root_post_idx + 1,                 # Right subtree starts after left subtree in postorder
76                post_end - 1                            # Right subtree ends before root in postorder
77            )
78          
79            return root
80      
81        # Create a mapping of values to their indices in postorder for O(1) lookup
82        postorder_index_map = {val: idx for idx, val in enumerate(postorder)}
83      
84        # Build the tree starting with full ranges
85        return build_tree(0, len(preorder) - 1, 0, len(postorder) - 1)
86
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    // Map to store the index position of each value in postorder array
18    private Map<Integer, Integer> postorderIndexMap = new HashMap<>();
19    // Store preorder array for global access
20    private int[] preorderArray;
21
22    /**
23     * Constructs a binary tree from preorder and postorder traversal arrays
24     * @param preorder The preorder traversal array
25     * @param postorder The postorder traversal array
26     * @return The root of the constructed binary tree
27     */
28    public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
29        this.preorderArray = preorder;
30      
31        // Build index map for quick lookup of postorder positions
32        for (int index = 0; index < postorder.length; index++) {
33            postorderIndexMap.put(postorder[index], index);
34        }
35      
36        // Start recursive construction with full range of both arrays
37        return buildTree(0, preorder.length - 1, 0, postorder.length - 1);
38    }
39
40    /**
41     * Recursively builds the tree using preorder and postorder ranges
42     * @param preStart Start index in preorder array
43     * @param preEnd End index in preorder array
44     * @param postStart Start index in postorder array
45     * @param postEnd End index in postorder array
46     * @return The root node of the subtree
47     */
48    private TreeNode buildTree(int preStart, int preEnd, int postStart, int postEnd) {
49        // Base case: empty range
50        if (preStart > preEnd) {
51            return null;
52        }
53      
54        // Create root node using first element in preorder range
55        TreeNode root = new TreeNode(preorderArray[preStart]);
56      
57        // Base case: single node (leaf)
58        if (preStart == preEnd) {
59            return root;
60        }
61      
62        // Find the position of left subtree root in postorder
63        // The left subtree root is the second element in preorder
64        int leftRootPostIndex = postorderIndexMap.get(preorderArray[preStart + 1]);
65      
66        // Calculate the size of left subtree
67        // Elements from postStart to leftRootPostIndex form the left subtree
68        int leftSubtreeSize = leftRootPostIndex - postStart + 1;
69      
70        // Recursively build left subtree
71        // Preorder range: [preStart + 1, preStart + leftSubtreeSize]
72        // Postorder range: [postStart, leftRootPostIndex]
73        root.left = buildTree(preStart + 1, preStart + leftSubtreeSize, 
74                             postStart, leftRootPostIndex);
75      
76        // Recursively build right subtree
77        // Preorder range: [preStart + leftSubtreeSize + 1, preEnd]
78        // Postorder range: [leftRootPostIndex + 1, postEnd - 1] (exclude root at postEnd)
79        root.right = buildTree(preStart + leftSubtreeSize + 1, preEnd, 
80                              leftRootPostIndex + 1, postEnd - 1);
81      
82        return root;
83    }
84}
85
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    TreeNode* constructFromPrePost(vector<int>& preorder, vector<int>& postorder) {
15        // Build a hash map to store the index of each value in postorder for O(1) lookup
16        unordered_map<int, int> postorderIndexMap;
17        int n = postorder.size();
18      
19        for (int i = 0; i < n; ++i) {
20            postorderIndexMap[postorder[i]] = i;
21        }
22      
23        // Recursive function to construct the tree
24        // Parameters:
25        // preStart, preEnd: start and end indices in preorder array
26        // postStart, postEnd: start and end indices in postorder array
27        function<TreeNode*(int, int, int, int)> buildTree = [&](int preStart, int preEnd, 
28                                                                 int postStart, int postEnd) -> TreeNode* {
29            // Base case: no elements to construct the tree
30            if (preStart > preEnd) {
31                return nullptr;
32            }
33          
34            // The first element in preorder is always the root
35            TreeNode* root = new TreeNode(preorder[preStart]);
36          
37            // Base case: only one element (leaf node)
38            if (preStart == preEnd) {
39                return root;
40            }
41          
42            // The second element in preorder is the root of left subtree (if it exists)
43            // Find its position in postorder to determine the boundary between left and right subtrees
44            int leftRootValue = preorder[preStart + 1];
45            int leftRootIndexInPost = postorderIndexMap[leftRootValue];
46          
47            // Calculate the size of left subtree
48            // All elements from postStart to leftRootIndexInPost form the left subtree
49            int leftSubtreeSize = leftRootIndexInPost - postStart + 1;
50          
51            // Recursively build left subtree
52            // Preorder: from (preStart + 1) to (preStart + leftSubtreeSize)
53            // Postorder: from postStart to leftRootIndexInPost
54            root->left = buildTree(preStart + 1, preStart + leftSubtreeSize, 
55                                 postStart, leftRootIndexInPost);
56          
57            // Recursively build right subtree
58            // Preorder: from (preStart + leftSubtreeSize + 1) to preEnd
59            // Postorder: from (leftRootIndexInPost + 1) to (postEnd - 1)
60            // Note: postEnd - 1 because the last element in postorder is the current root
61            root->right = buildTree(preStart + leftSubtreeSize + 1, preEnd, 
62                                  leftRootIndexInPost + 1, postEnd - 1);
63          
64            return root;
65        };
66      
67        // Start the recursive construction with the full range of both arrays
68        return buildTree(0, n - 1, 0, n - 1);
69    }
70};
71
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Constructs a binary tree from preorder and postorder traversal arrays
17 * @param preorder - Array representing preorder traversal of the tree
18 * @param postorder - Array representing postorder traversal of the tree
19 * @returns The root node of the constructed binary tree
20 */
21function constructFromPrePost(preorder: number[], postorder: number[]): TreeNode | null {
22    // Create a map to store the index positions of values in postorder array
23    const postorderIndexMap: Map<number, number> = new Map();
24    const arrayLength: number = postorder.length;
25  
26    // Build the index map for quick lookup of positions in postorder
27    for (let i = 0; i < arrayLength; i++) {
28        postorderIndexMap.set(postorder[i], i);
29    }
30  
31    /**
32     * Recursively builds the tree using preorder and postorder segments
33     * @param preorderStartIndex - Starting index in preorder array for current subtree
34     * @param postorderStartIndex - Starting index in postorder array for current subtree
35     * @param subtreeSize - Number of nodes in the current subtree
36     * @returns Root node of the constructed subtree
37     */
38    const buildSubtree = (
39        preorderStartIndex: number, 
40        postorderStartIndex: number, 
41        subtreeSize: number
42    ): TreeNode | null => {
43        // Base case: empty subtree
44        if (subtreeSize <= 0) {
45            return null;
46        }
47      
48        // Create root node using the first element in preorder segment
49        const rootNode: TreeNode = new TreeNode(preorder[preorderStartIndex]);
50      
51        // Base case: single node (leaf)
52        if (subtreeSize === 1) {
53            return rootNode;
54        }
55      
56        // Find the position of left subtree root in postorder array
57        // The left subtree root is the second element in preorder (index + 1)
58        const leftRootPostorderIndex: number = postorderIndexMap.get(preorder[preorderStartIndex + 1])!;
59      
60        // Calculate the size of left subtree
61        // All elements from postorderStartIndex to leftRootPostorderIndex (inclusive) belong to left subtree
62        const leftSubtreeSize: number = leftRootPostorderIndex - postorderStartIndex + 1;
63      
64        // Recursively build left subtree
65        rootNode.left = buildSubtree(
66            preorderStartIndex + 1,           // Left subtree starts right after root in preorder
67            postorderStartIndex,               // Left subtree starts at beginning of postorder segment
68            leftSubtreeSize                    // Size of left subtree
69        );
70      
71        // Recursively build right subtree
72        rootNode.right = buildSubtree(
73            preorderStartIndex + 1 + leftSubtreeSize,  // Right subtree starts after left subtree in preorder
74            leftRootPostorderIndex + 1,                // Right subtree starts after left subtree in postorder
75            subtreeSize - 1 - leftSubtreeSize          // Remaining nodes belong to right subtree
76        );
77      
78        return rootNode;
79    };
80  
81    // Start building the tree from the entire arrays
82    return buildSubtree(0, 0, arrayLength);
83}
84

Time and Space Complexity

Time Complexity: O(n)

The algorithm constructs a binary tree from preorder and postorder traversals. Breaking down the time complexity:

  1. Creating the pos dictionary takes O(n) time, where we iterate through all elements in the postorder array once.

  2. The dfs function visits each node exactly once during the tree construction. For each node:

    • Creating a new TreeNode takes O(1) time
    • Looking up the position in the pos dictionary takes O(1) time
    • Calculating the split point takes O(1) time
    • Making recursive calls for left and right subtrees

Since each of the n nodes is processed exactly once with O(1) operations per node, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity consists of:

  1. The pos dictionary storing positions of all n elements from the postorder array: O(n) space

  2. The recursion call stack depth, which in the worst case (skewed tree) can go up to O(n) levels deep

  3. The output tree structure itself contains n nodes: O(n) space

Therefore, the total space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Assuming Left Child Always Exists

The Pitfall: The most critical error in this solution is assuming that preorder[pre_start + 1] is always the left child. When a node has only a right child (no left child), preorder[pre_start + 1] would actually be the right child, not the left child. This assumption causes the algorithm to incorrectly partition the arrays and construct an invalid tree.

Example of Failure:

Tree:     1
           \
            2
             \
              3

Preorder:  [1, 2, 3]
Postorder: [3, 2, 1]

In this case, preorder[1] = 2 is the right child of 1, not the left child. The algorithm would incorrectly try to build 2 as the left child.

Solution: Check if the potential left child actually exists by verifying if it could be a left child:

def build_tree(pre_start, pre_end, post_start, post_end):
    if pre_start > pre_end:
        return None
  
    root = TreeNode(preorder[pre_start])
  
    if pre_start == pre_end:
        return root
  
    # Check if left child exists
    # If preorder[pre_start + 1] appears right before the root in postorder,
    # it means there's no left subtree (it's actually the right child)
    potential_left_val = preorder[pre_start + 1]
    potential_left_idx = postorder_index_map[potential_left_val]
  
    # If the potential left child is at post_end - 1, it's actually a right child
    if potential_left_idx == post_end - 1:
        # No left child, only right child
        root.right = build_tree(
            pre_start + 1, 
            pre_end, 
            post_start, 
            post_end - 1
        )
    else:
        # Normal case: has left child (and possibly right child)
        left_subtree_size = potential_left_idx - post_start + 1
      
        root.left = build_tree(
            pre_start + 1,
            pre_start + left_subtree_size,
            post_start,
            potential_left_idx
        )
      
        root.right = build_tree(
            pre_start + left_subtree_size + 1,
            pre_end,
            potential_left_idx + 1,
            post_end - 1
        )
  
    return root

2. Off-by-One Errors in Range Calculations

The Pitfall: Incorrectly calculating the boundaries for recursive calls, especially forgetting to exclude the root from postorder's right subtree range (post_end - 1 instead of post_end).

Solution: Always remember:

  • In preorder: root is at the start of the range
  • In postorder: root is at the end of the range
  • Right subtree in postorder ends at post_end - 1 (excluding the current root)

3. Not Handling Edge Cases

The Pitfall: Forgetting to handle single-node trees or empty subtrees properly.

Solution: Always include both base cases:

  • Empty range: if pre_start > pre_end: return None
  • Single node: if pre_start == pre_end: return root

These checks prevent index out of bounds errors and ensure correct tree construction for all cases.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More