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 valuespostorder
: 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.
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]
topostorder[i]
form the left subtree - Elements from
postorder[i+1]
topostorder[-2]
form the right subtree (excluding the main root atpostorder[-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 ispreorder[1+m:]
- In postorder: left subtree is
postorder[0:m]
, right subtree ispostorder[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:
-
Base Case - Empty Range: If
a > b
, the range is empty, returnNone
. -
Create Root Node: The root is always at
preorder[a]
:root = TreeNode(preorder[a])
-
Base Case - Single Node: If
a == b
, this is a leaf node with no children, return the root directly. -
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
. - The left subtree's root (if it exists) is
-
Recursively Build Subtrees:
- Left subtree:
- Preorder range:
[a + 1, a + m]
(skip root, take nextm
elements) - Postorder range:
[c, i]
(from start to left subtree root)
- Preorder range:
- 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)
- Preorder range:
root.left = dfs(a + 1, a + m, c, i) root.right = dfs(a + m + 1, b, i + 1, d - 1)
- Left subtree:
-
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 EvaluatorExample 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:
-
Creating the
pos
dictionary takesO(n)
time, where we iterate through all elements in the postorder array once. -
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 takesO(1)
time - Calculating the split point takes
O(1)
time - Making recursive calls for left and right subtrees
- Creating a new TreeNode takes
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:
-
The
pos
dictionary storing positions of alln
elements from the postorder array:O(n)
space -
The recursion call stack depth, which in the worst case (skewed tree) can go up to
O(n)
levels deep -
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.
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
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!