1028. Recover a Tree From Preorder Traversal
Problem Description
You are given a string that represents the output of a preorder depth-first search (DFS) traversal of a binary tree. In this traversal output:
- Each node is represented by a certain number of dashes followed by its value
- The number of dashes indicates the depth of the node in the tree
- The root node has depth 0 (no dashes)
- Each child node has depth = parent's depth + 1
For example, if you see "1-2--3"
, this means:
1
is at depth 0 (root)2
is at depth 1 (child of 1)3
is at depth 2 (child of 2)
The traversal follows preorder DFS, meaning nodes are visited in this order:
- Visit the current node
- Visit the left subtree
- Visit the right subtree
An important constraint: if a node has only one child, that child must be the left child.
Your task is to reconstruct the original binary tree from this traversal string and return the root node.
Example walkthrough:
Given input "1-2--3--4-5--6--7"
:
1
(0 dashes) - root2
(1 dash) - left child of 13
(2 dashes) - left child of 24
(2 dashes) - right child of 25
(1 dash) - right child of 16
(2 dashes) - left child of 57
(2 dashes) - right child of 5
This reconstructs to:
1 / \ 2 5 / \ / \ 3 4 6 7
The solution uses a stack to keep track of the path from root to the current position. When processing each node:
- Count the dashes to determine depth
- Extract the node value
- Pop nodes from stack until stack size equals the target depth
- Attach the new node as left child if parent has no left child, otherwise as right child
- Push the new node onto the stack for future processing
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a binary tree, which is a special type of graph (specifically, a connected acyclic graph with hierarchical structure).
Is it a tree?
- Yes: The problem explicitly deals with a binary tree structure where we need to reconstruct the tree from its preorder DFS traversal output.
DFS
- Yes: The problem directly mentions that the traversal is a preorder depth-first search (DFS). The input string represents the output of a DFS traversal where:
- Nodes are visited in preorder (root, then left subtree, then right subtree)
- The depth information (number of dashes) tells us the level of each node
- We need to use this DFS traversal pattern to reconstruct the original tree
Conclusion: The flowchart leads us directly to using DFS as the core pattern. The problem is fundamentally about understanding and reversing a DFS traversal. While the actual solution uses a stack-based iterative approach rather than recursive DFS, it's still following the DFS pattern by:
- Processing nodes in the order they appear in the preorder traversal
- Using depth information to maintain the correct parent-child relationships
- Building the tree following the same DFS path that generated the original traversal string
Intuition
The key insight is that in a preorder DFS traversal, we always process nodes in a specific order: parent first, then its children. When we see the traversal string, we're essentially seeing the exact path the DFS took through the tree.
Think about how DFS works when traversing a tree:
- We start at the root
- We go as deep as possible along the left path
- When we can't go deeper, we backtrack and explore right branches
The number of dashes tells us exactly how deep we are in the tree. This is crucial because:
- If we see a node with
d
dashes, it must be at depthd
- If the next node has
d+1
dashes, it's a child of the current node - If the next node has the same or fewer dashes, we need to backtrack
Consider this simple example: "1-2--3-4"
1
(depth 0): root2
(depth 1): must be child of 13
(depth 2): must be child of 24
(depth 1): can't be child of 3 (depth decreased), must backtrack to 1 and be its other child
This naturally suggests using a stack to track our current path from root:
- When depth increases: we're going deeper (current node's child)
- When depth stays same or decreases: we need to backtrack (pop from stack)
The stack always maintains the path from root to the current position. When we need to attach a new node:
- We pop nodes until the stack size matches the target depth (this is our backtracking)
- The top of the stack is now the parent of our new node
- We attach as left child if it's empty, otherwise as right child (following the constraint that single children must be left children)
This approach directly mirrors how we would manually trace through the DFS traversal - keeping track of where we are in the tree and backtracking when necessary based on the depth indicators.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The implementation uses a stack-based approach to reconstruct the tree from its preorder traversal string. Here's how the algorithm works:
Data Structure:
- A
Stack<TreeNode>
to maintain the current path from root to the node being processed
Algorithm Steps:
-
Parse the traversal string using an index pointer
i
:while (i < traversal.length())
-
Extract depth information by counting consecutive dashes:
int depth = 0; while (i < traversal.length() && traversal.charAt(i) == '-') { depth++; i++; }
-
Extract the node value by reading consecutive digits:
int num = 0; while (i < traversal.length() && Character.isDigit(traversal.charAt(i))) { num = num * 10 + (traversal.charAt(i) - '0'); i++; }
-
Create the new node with the extracted value:
TreeNode newNode = new TreeNode(num);
-
Adjust stack to match target depth (backtracking):
while (stack.size() > depth) { stack.pop(); }
This ensures the stack size equals the depth, meaning the top of the stack is the parent of the new node.
-
Attach the new node to its parent:
if (!stack.isEmpty()) { if (stack.peek().left == null) { stack.peek().left = newNode; } else { stack.peek().right = newNode; } }
Following the rule that if a node has only one child, it must be the left child.
-
Push the new node onto the stack for future processing:
stack.push(newNode);
-
Return the root (first element in the stack):
return stack.isEmpty() ? null : stack.get(0);
Example Walkthrough:
For input "1-2--3"
:
- Parse
1
(depth=0): Create node(1), stack is empty so this is root, push to stack:[1]
- Parse
2
(depth=1): Create node(2), stack size (1) matches depth (1), attach as left child of node(1), push to stack:[1, 2]
- Parse
3
(depth=2): Create node(3), stack size (2) matches depth (2), attach as left child of node(2), push to stack:[1, 2, 3]
Final tree:
1
/
2
/
3
The beauty of this approach is that the stack naturally handles the backtracking needed when moving between different branches of the tree, perfectly mirroring the DFS traversal pattern.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the input "1-2--4-3"
step by step to understand how the stack-based approach reconstructs the tree.
Initial State:
- Stack:
[]
- Index
i = 0
Step 1: Process "1"
- Count dashes: 0 dashes → depth = 0
- Extract value: 1
- Create node(1)
- Stack size (0) matches depth (0), so no popping needed
- Stack is empty, so this is our root
- Push node(1) to stack
- Stack:
[node(1)]
Step 2: Process "-2"
- Count dashes: 1 dash → depth = 1
- Extract value: 2
- Create node(2)
- Stack size (1) matches depth (1), so no popping needed
- Parent is node(1) (top of stack)
- node(1).left is null, so attach node(2) as left child
- Push node(2) to stack
- Stack:
[node(1), node(2)]
- Tree so far:
1 / 2
Step 3: Process "--4"
- Count dashes: 2 dashes → depth = 2
- Extract value: 4
- Create node(4)
- Stack size (2) matches depth (2), so no popping needed
- Parent is node(2) (top of stack)
- node(2).left is null, so attach node(4) as left child
- Push node(4) to stack
- Stack:
[node(1), node(2), node(4)]
- Tree so far:
1
/
2
/
4
Step 4: Process "-3"
- Count dashes: 1 dash → depth = 1
- Extract value: 3
- Create node(3)
- Stack size (3) > depth (1), so pop until size equals 1
- Pop node(4): Stack:
[node(1), node(2)]
- Pop node(2): Stack:
[node(1)]
- Pop node(4): Stack:
- Now stack size (1) matches depth (1)
- Parent is node(1) (top of stack)
- node(1).left is not null (it's node(2)), so attach node(3) as right child
- Push node(3) to stack
- Stack:
[node(1), node(3)]
Final Tree:
1 / \ 2 3 / 4
The algorithm successfully reconstructed the tree by:
- Using depth to determine parent-child relationships
- Using the stack to maintain the current path and handle backtracking
- Following the left-child-first rule when attaching nodes
Solution Implementation
1# Definition for a binary tree node
2class TreeNode:
3 def __init__(self, val=0, left=None, right=None):
4 self.val = val
5 self.left = left
6 self.right = right
7
8
9def recoverFromPreorder(traversal: str) -> TreeNode:
10 """
11 Recovers a binary tree from its preorder traversal string representation.
12 The traversal string uses dashes to indicate depth levels.
13
14 Args:
15 traversal: The preorder traversal string with depth indicators
16
17 Returns:
18 The root node of the recovered binary tree
19 """
20 # Stack to maintain the current path from root to the node being processed
21 node_stack = []
22 current_index = 0
23
24 while current_index < len(traversal):
25 # Count the number of dashes to determine the depth of the current node
26 current_depth = 0
27 while current_index < len(traversal) and traversal[current_index] == '-':
28 current_depth += 1
29 current_index += 1
30
31 # Parse the node value from the traversal string
32 node_value = 0
33 while current_index < len(traversal) and traversal[current_index].isdigit():
34 node_value = node_value * 10 + int(traversal[current_index])
35 current_index += 1
36
37 # Create a new tree node with the parsed value
38 current_node = TreeNode(node_value)
39
40 # Pop nodes from stack until we reach the correct depth
41 # This ensures the stack contains only ancestors of the current node
42 while len(node_stack) > current_depth:
43 node_stack.pop()
44
45 # Attach the current node to its parent
46 if node_stack:
47 parent_node = node_stack[-1]
48 # If left child is empty, attach as left child; otherwise, attach as right child
49 if parent_node.left is None:
50 parent_node.left = current_node
51 else:
52 parent_node.right = current_node
53
54 # Add the current node to the stack for future processing
55 node_stack.append(current_node)
56
57 # Return the root node (first element in stack) or None if stack is empty
58 return node_stack[0] if node_stack else None
59
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 public TreeNode recoverFromPreorder(String traversal) {
18 // Stack to maintain the path from root to current node
19 Stack<TreeNode> stack = new Stack<>();
20 int index = 0;
21
22 // Process the entire traversal string
23 while (index < traversal.length()) {
24 // Count the number of dashes to determine the depth
25 int currentDepth = 0;
26 while (index < traversal.length() && traversal.charAt(index) == '-') {
27 currentDepth++;
28 index++;
29 }
30
31 // Parse the node value (multi-digit numbers supported)
32 int nodeValue = 0;
33 while (index < traversal.length() && Character.isDigit(traversal.charAt(index))) {
34 nodeValue = nodeValue * 10 + (traversal.charAt(index) - '0');
35 index++;
36 }
37
38 // Create a new node with the parsed value
39 TreeNode currentNode = new TreeNode(nodeValue);
40
41 // Adjust stack size to match current depth
42 // Pop nodes until stack size equals current depth
43 while (stack.size() > currentDepth) {
44 stack.pop();
45 }
46
47 // Connect the new node to its parent
48 if (!stack.isEmpty()) {
49 TreeNode parentNode = stack.peek();
50 // If left child is empty, attach as left child
51 if (parentNode.left == null) {
52 parentNode.left = currentNode;
53 }
54 // Otherwise, attach as right child
55 else {
56 parentNode.right = currentNode;
57 }
58 }
59
60 // Push current node onto stack for potential future children
61 stack.push(currentNode);
62 }
63
64 // Return the root node (first element in stack) or null if empty
65 return stack.isEmpty() ? null : stack.get(0);
66 }
67}
68
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
8 * };
9 */
10class Solution {
11public:
12 TreeNode* recoverFromPreorder(string S) {
13 stack<TreeNode*> nodeStack; // Stack to maintain nodes at each depth level
14 int currentDepth = 0; // Current depth level (number of dashes)
15 int currentNumber = 0; // Current node value being parsed
16
17 for (int i = 0; i < S.length(); ++i) {
18 // Count consecutive dashes to determine depth
19 if (S[i] == '-') {
20 currentDepth++;
21 }
22 // Parse digits to build the node value
23 else {
24 currentNumber = currentNumber * 10 + (S[i] - '0');
25 }
26
27 // Check if we've finished parsing a complete node
28 // (either at string end or transitioning from digit to dash)
29 if (i + 1 >= S.length() || (isdigit(S[i]) && S[i + 1] == '-')) {
30 // Create new node with the parsed value
31 TreeNode* newNode = new TreeNode(currentNumber);
32
33 // Pop nodes from stack until we reach the correct parent depth
34 // Parent should be at depth = currentDepth - 1
35 while (nodeStack.size() > currentDepth) {
36 nodeStack.pop();
37 }
38
39 // Attach new node to its parent (if exists)
40 if (!nodeStack.empty()) {
41 TreeNode* parent = nodeStack.top();
42 // Attach as left child if available, otherwise as right child
43 if (parent->left == nullptr) {
44 parent->left = newNode;
45 } else {
46 parent->right = newNode;
47 }
48 }
49
50 // Push the new node onto stack for potential future children
51 nodeStack.push(newNode);
52
53 // Reset counters for next node
54 currentDepth = 0;
55 currentNumber = 0;
56 }
57 }
58
59 // Return the root node (bottom of stack)
60 TreeNode* root = nullptr;
61 while (!nodeStack.empty()) {
62 root = nodeStack.top();
63 nodeStack.pop();
64 }
65
66 return root;
67 }
68};
69
1/**
2 * Definition for a binary tree node
3 */
4class TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8 constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
9 this.val = (val === undefined ? 0 : val);
10 this.left = (left === undefined ? null : left);
11 this.right = (right === undefined ? null : right);
12 }
13}
14
15/**
16 * Recovers a binary tree from its preorder traversal string representation
17 * The traversal string uses dashes to indicate depth levels
18 *
19 * @param traversal - The preorder traversal string with depth indicators
20 * @returns The root node of the recovered binary tree
21 */
22function recoverFromPreorder(traversal: string): TreeNode | null {
23 // Stack to maintain the current path from root to the node being processed
24 const nodeStack: TreeNode[] = [];
25 let currentIndex: number = 0;
26
27 while (currentIndex < traversal.length) {
28 // Count the number of dashes to determine the depth of the current node
29 let currentDepth: number = 0;
30 while (currentIndex < traversal.length && traversal[currentIndex] === '-') {
31 currentDepth++;
32 currentIndex++;
33 }
34
35 // Parse the node value from the traversal string
36 let nodeValue: number = 0;
37 while (currentIndex < traversal.length && traversal[currentIndex] >= '0' && traversal[currentIndex] <= '9') {
38 nodeValue = nodeValue * 10 + Number(traversal[currentIndex]);
39 currentIndex++;
40 }
41
42 // Create a new tree node with the parsed value
43 const currentNode: TreeNode = new TreeNode(nodeValue);
44
45 // Pop nodes from stack until we reach the correct depth
46 // This ensures the stack contains only ancestors of the current node
47 while (nodeStack.length > currentDepth) {
48 nodeStack.pop();
49 }
50
51 // Attach the current node to its parent
52 if (nodeStack.length > 0) {
53 const parentNode: TreeNode = nodeStack[nodeStack.length - 1];
54 // If left child is empty, attach as left child; otherwise, attach as right child
55 if (parentNode.left === null) {
56 parentNode.left = currentNode;
57 } else {
58 parentNode.right = currentNode;
59 }
60 }
61
62 // Add the current node to the stack for future processing
63 nodeStack.push(currentNode);
64 }
65
66 // Return the root node (first element in stack) or null if stack is empty
67 return nodeStack.length > 0 ? nodeStack[0] : null;
68}
69
Time and Space Complexity
Time Complexity: O(n)
The algorithm iterates through the input string traversal
exactly once, where n
is the length of the string. During this traversal:
- Each character is visited once to count dashes (depth calculation)
- Each character is visited once to parse numbers
- The stack operations (push/pop) for each node are performed at most once
While there's a nested loop for popping from the stack (while (stack.size() > depth)
), each node is pushed once and popped at most once throughout the entire execution, making the amortized cost O(1)
per node. Since the number of nodes is proportional to the length of the string (actually less than n
), the overall time complexity remains O(n)
.
Space Complexity: O(h)
The space complexity is determined by:
- The stack that stores TreeNode references, which at any point contains nodes along the current path from root to the node being processed
- The maximum size of the stack equals the height
h
of the tree being constructed - In the worst case (completely skewed tree),
h = O(n)
wheren
is the number of nodes - In the best case (balanced tree),
h = O(log n)
The additional space used for variables like i
, depth
, and num
is O(1)
.
Therefore, the space complexity is O(h)
where h
is the height of the resulting tree.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Depth Calculation Due to Off-by-One Errors
A common mistake is misunderstanding the relationship between stack size and depth. Developers often incorrectly pop nodes from the stack, leading to wrong parent-child relationships.
Pitfall Example:
# WRONG: Popping until stack size equals depth - 1
while len(node_stack) > current_depth - 1:
node_stack.pop()
Why it's wrong: The depth represents the exact level in the tree. If depth is 2, we need exactly 2 ancestors in the stack (root at depth 0, parent at depth 1). The current node will be at depth 2.
Solution: Always pop until len(node_stack) == current_depth
:
# CORRECT: Stack size should equal the depth
while len(node_stack) > current_depth:
node_stack.pop()
2. Parsing Multi-Digit Numbers Incorrectly
Another frequent error is assuming all node values are single digits, which fails for values like 10, 123, etc.
Pitfall Example:
# WRONG: Only reading a single digit
node_value = int(traversal[current_index])
current_index += 1
Solution: Continue parsing while encountering digits:
# CORRECT: Parse all consecutive digits
node_value = 0
while current_index < len(traversal) and traversal[current_index].isdigit():
node_value = node_value * 10 + int(traversal[current_index])
current_index += 1
3. Not Handling Edge Cases Properly
Failing to handle empty input or single-node trees can cause index errors or null pointer exceptions.
Pitfall Example:
# WRONG: Assuming stack always has elements return node_stack[0] # Crashes if traversal is empty string
Solution: Check for empty stack:
# CORRECT: Handle empty input gracefully return node_stack[0] if node_stack else None
4. Misunderstanding the Single Child Rule
Some implementations incorrectly place a single child as the right child, violating the constraint that single children must be left children.
Pitfall Example:
# WRONG: Randomly choosing left or right for single child if parent_node.left is None and parent_node.right is None: # Might incorrectly assign to right child parent_node.right = current_node
Solution: Always check left child first:
# CORRECT: Single child must be left child if parent_node.left is None: parent_node.left = current_node else: parent_node.right = current_node
5. Stack Management During Backtracking
A subtle but critical mistake is not properly maintaining the stack when backtracking to different branches.
Pitfall Example:
# WRONG: Forgetting to add current node to stack
while len(node_stack) > current_depth:
node_stack.pop()
# Missing: node_stack.append(current_node)
This breaks the algorithm for subsequent nodes because the stack doesn't accurately represent the path from root to current position.
Solution: Always push the current node after processing:
# CORRECT: Maintain stack integrity
while len(node_stack) > current_depth:
node_stack.pop()
# ... attach node to parent ...
node_stack.append(current_node) # Don't forget this!
Which algorithm should you use to find a node that is close to the root of the tree?
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 dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
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!