Start learning free

LESSON 355 OF 445 · BINARY TREES

Recover a Tree From Preorder Traversal in Rust: Stack in One Pass

Practice O(n) Read the full explanation

We walk a binary tree in preorder: first a node, then its whole left subtree, then its whole right subtree. For every node we write D dashes, where D is the node's depth, followed right away by the node's value. The root has depth 0, so it has no dashes; its children have depth 1, their children depth 2, and so on.

If a node has only one child, that child is its left child.

Given the string traversal written this way, rebuild the tree and return its root.

The tests print the tree you return level by level, from left to right, with null where a child is missing and the trailing nulls left out. For example, [1,2,null,3] is the tree with root 1, 2 as its left child and 3 as the left child of 2.

 

Example 1:

Input: traversal = "1-2--3--4-5--6--7"
Output: [1,2,5,3,4,6,7]
       1
     /   \
    2     5
   / \   / \
  3   4 6   7

Explanation: "-2" and "-5" have one dash: they are the root's children, 2 first, so 2 is the left one. "--3" and "--4" come right after "-2", so they are the children of 2; "--6" and "--7" come after "-5", so they are the children of 5.

Example 2:

Input: traversal = "1-2--3---4-5--6---7"
Output: [1,2,5,3,null,6,null,4,null,7]
        1
       / \
      2   5
     /   /
    3   6
   /   /
  4   7

Explanation: After "---4" (depth 3) comes "-5" (depth 1): the depth goes back up by two levels at once, so 5 is the second child of the root, its right child. Every node with a single child has it on the left.

Example 3:

Input: traversal = "5-12--7---40--9"
Output: [5,12,null,7,9,40]
      5
     /
    12
   /  \
  7    9
 /
40

Explanation: Values can have several digits: the dashes before a node end where its digits start. The root 5 has a single child, 12, so it is a left child.

 

Constraints:

  • The number of nodes in the tree is in the range [1, 1000].
  • Every node's value is between 1 and 109.
  • traversal is a valid preorder walk of a tree, written as described above.
STEP 1 OF 20
    Ctrl + Enter to check

    RUST · BINARY TREES

    Recover Tree From Preorder in Rust, explained

    Problem overview

    A binary tree was written as a string by walking it in preorder: a node first, then its whole left subtree, then its whole right subtree. Before each node's value the walk writes one dash per level of depth, so the root has none, its children have one, their children two, and so on. When a node has a single child, that child is the left one.

    The function recover_from_preorder receives that string, traversal, and must rebuild the tree and return its root as an Option<Box<TreeNode>>.

    For "1-2--3--4-5--6--7" the nodes are 1 at depth 0, then 2 at depth 1, 3 and 4 at depth 2, then 5 at depth 1, and 6 and 7 at depth 2. The tree has 1 as the root, 2 and 5 as its children, 3 and 4 under 2, and 6 and 7 under 5. Values can have several digits, as in "5-12--7---40--9": the dashes before a value end where its digits start.

    Approach: keep the current path on a stack

    Because the string is a preorder walk, the node being read always belongs somewhere along the path from the root to the node read just before it. The node's depth tells exactly where: its parent is the node at depth depth - 1 on that path.

    So the code keeps a stack of the nodes on the current path, with the root at the bottom. The node at position d of the stack has depth d. For each new node it does two things:

    1. Pops the stack down to depth nodes. The popped nodes are finished: everything after them in the string is shallower, so their subtrees are complete.
    2. Pushes the new node. The node now on top of the stack is its parent.

    In Rust there is one wrinkle. A Box has a single owner, so a node can't sit in the stack and in its parent's left field at the same time. The solution makes the stack own the nodes of the path and moves a node into its parent only when it is popped. A parent's first finished child becomes its left child and the second becomes its right child, which also puts a single child on the left, as the statement requires.

    Take "1-2--3---4-5--6---7". After reading 4 the stack is 1, 2, 3, 4. The next node, 5, has depth 1, so the stack must shrink to one node. Popping 4 attaches it to 3, popping 3 attaches it to 2, and popping 2 attaches it to 1. Then 5 is pushed. The tree ends up as [1, 2, 5, 3, null, 6, null, 4, null, 7], with 3 and 6 as left children because they are the only child of their parents.

    Step by step

    1. Read the string by index. traversal.as_bytes() gives a byte slice. The string holds only digits and dashes, one byte each, so bytes[i] is the character at position i.
    2. Create the stack as an empty Vec<Box<TreeNode>>.
    3. Create the position i at 0. Every loop that reads a character advances it, so the string is read once.
    4. Outer loop: run while i < bytes.len(). Each pass reads exactly one node, its dashes and then its digits.
    5. Count the dashes. depth starts at 0, and an inner loop adds 1 and moves i forward while bytes[i] is b'-'. It needs no bounds check, because a value always follows the dashes.
    6. Read the value. value starts at 0. While i is inside the string and the byte isn't a dash, multiply value by 10 and add the digit. Reading 4 then 0 gives 4, then 40.
    7. Fix the stack. Call the helper Self::pop_to(&mut stack, depth) so that only the new node's ancestors remain.
    8. Push the new node, a Box around TreeNode::new(value).
    9. Attach the last path. After the loop the stack still holds the final path. pop_to with size 1 attaches all of it and leaves only the root.
    10. Return the root. stack.pop() already returns an Option, so it needs no wrapping.
    11. Write pop_to. It takes the stack as &mut Vec<Box<TreeNode>> and a size: usize. While the stack is longer than size, it pops child, borrows the new top as parent with last_mut(), and moves the child into parent.left if that is empty, or into parent.right otherwise.

    The solution in Rust

    impl Solution {
        fn pop_to(stack: &mut Vec<Box<TreeNode>>, size: usize) {
            while stack.len() > size {
                let child = stack.pop().unwrap();
                let parent = stack.last_mut().unwrap();
                if parent.left.is_none() {
                    parent.left = Some(child);
                } else {
                    parent.right = Some(child);
                }
            }
        }
    
        fn recover_from_preorder(traversal: String) -> Option<Box<TreeNode>> {
            let bytes = traversal.as_bytes();
            let mut stack: Vec<Box<TreeNode>> = Vec::new();
            let mut i = 0;
            while i < bytes.len() {
                let mut depth = 0;
                while bytes[i] == b'-' {
                    depth += 1;
                    i += 1;
                }
                let mut value = 0;
                while i < bytes.len() && bytes[i] != b'-' {
                    value = value * 10 + (bytes[i] - b'0') as i32;
                    i += 1;
                }
                Self::pop_to(&mut stack, depth);
                stack.push(Box::new(TreeNode::new(value)));
            }
            Self::pop_to(&mut stack, 1);
            stack.pop()
        }
    }
    

    Rust details worth noticing:

    • Bytes, not characters. Indexing a String by number isn't allowed in Rust, but a &[u8] can be indexed. That is safe here because every character is ASCII. The literal b'-' is a u8, so it compares directly with bytes[i].
    • Digit arithmetic. bytes[i] - b'0' is a u8 between 0 and 9, and as i32 converts it to the type of value. Values go up to 1 000 000 000, which fits in an i32.
    • Inferred types. depth has no annotation, but passing it to pop_to, which wants a usize, makes it a usize. value becomes an i32 from the addition.
    • Passing the stack. &mut stack lends the vector to the helper for the duration of the call, and stack is usable again afterwards. The helper has no self, so it is called as Self::pop_to.
    • Moving a box. parent.left = Some(child) moves child into the parent. After that line child can't be used again, which is why the if and else branches each use it once.
    • last_mut(). It gives Option<&mut Box<TreeNode>>. The unwrap() is safe because the loop only runs while the stack is longer than size, and size is at least 1 whenever a pop happens: the root is never popped.

    Complexity

    Time: O(n), where n is the length of the string. Each character is read once, and each node is pushed once and popped once, with constant work for each.

    Space: O(h) beyond the tree itself, where h is the tree's height, because the stack holds one root-to-node path. For a tree that is a single chain, that is the number of nodes.

    Edge cases and common mistakes

    • A single node. "7" pushes one node, the final pop_to(.., 1) does nothing, and the root is returned.
    • Multi-digit values. Reading one byte as one value breaks "5-12--7". Keep reading until the next dash or the end.
    • Two children at the same depth. "1-2-3" gives the root 1 with 2 on the left and 3 on the right. Popping 2 fills the empty left slot, and popping 3 finds it occupied.
    • A jump up by several levels. After ---4 comes -5: depth goes from 3 to 1. Popping a single node isn't enough, so pop_to loops until the stack has the right size.
    • Forgetting the final attachment. Without the last pop_to, the nodes of the last path stay unattached, and stack.pop() returns the last node read instead of the root.
    • Checking i in the dash loop. It isn't needed, but in the digit loop i < bytes.len() must come first, or the last value runs past the end of the string and panics.

    Testing

    • "1-2--3--4-5--6--7" gives [1,2,5,3,4,6,7].
    • "1-2--3---4-5--6---7" gives [1,2,5,3,null,6,null,4,null,7].
    • "5-12--7---40--9" gives [5,12,null,7,9,40].
    • "7" gives [7].
    • "1-2-3" gives [1,2,3].
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free