Facebook Pixel

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:

  1. Visit the current node
  2. Visit the left subtree
  3. 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) - root
  • 2 (1 dash) - left child of 1
  • 3 (2 dashes) - left child of 2
  • 4 (2 dashes) - right child of 2
  • 5 (1 dash) - right child of 1
  • 6 (2 dashes) - left child of 5
  • 7 (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:

  1. Count the dashes to determine depth
  2. Extract the node value
  3. Pop nodes from stack until stack size equals the target depth
  4. Attach the new node as left child if parent has no left child, otherwise as right child
  5. 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:

  1. Processing nodes in the order they appear in the preorder traversal
  2. Using depth information to maintain the correct parent-child relationships
  3. Building the tree following the same DFS path that generated the original traversal string
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. We start at the root
  2. We go as deep as possible along the left path
  3. 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 depth d
  • 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): root
  • 2 (depth 1): must be child of 1
  • 3 (depth 2): must be child of 2
  • 4 (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:

  1. We pop nodes until the stack size matches the target depth (this is our backtracking)
  2. The top of the stack is now the parent of our new node
  3. 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:

  1. Parse the traversal string using an index pointer i:

    while (i < traversal.length())
  2. Extract depth information by counting consecutive dashes:

    int depth = 0;
    while (i < traversal.length() && traversal.charAt(i) == '-') {
        depth++;
        i++;
    }
  3. 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++;
    }
  4. Create the new node with the extracted value:

    TreeNode newNode = new TreeNode(num);
  5. 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.

  6. 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.

  7. Push the new node onto the stack for future processing:

    stack.push(newNode);
  8. 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 Evaluator

Example 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)]
  • 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:

  1. Using depth to determine parent-child relationships
  2. Using the stack to maintain the current path and handle backtracking
  3. 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) where n 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!
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More