Start learning free

LESSON 328 OF 414 · BINARY TREES

Zigzag Tree Traversal in Rust: DFS by Level, Then Reverse

Practice O(n) Read the full explanation

Given a binary tree, return the zigzag level order traversal of its nodes' values. (ie, from left to right, then right to left for the next level and alternate between).

Example:

Input: root = [3, 9, 20, None, None, 15, 7]
    3
   / \
  9  20
    /  \
   15   7

Output: 
[
  [3],
  [20,9],
  [15,7]
]

STEP 1 OF 9
    Ctrl + Enter to check

    RUST · BINARY TREES

    Zigzag Tree Traversal in Rust, explained

    Problem overview

    The function zigzag_traversal receives the root of a binary tree and returns a Vec<Vec<i32>> with one inner vector per level. The first level is read from left to right, the next from right to left, and so on, alternating.

    For the tree with root 3, children 9 and 20, and 20's children 15 and 7, the answer is:

    [[3], [20, 9], [15, 7]]

    Level 0 is [3]. Level 1 holds 9 and 20 from left to right, so it is reversed to [20, 9]. Level 2 holds 15 and 7, which is read left to right again.

    Approach: collect levels left to right, flip every second one

    The usual level-by-level technique is a breadth-first search with a queue. This lesson takes another route that needs no queue. A depth-first search that carries the current level number can still build the lists level by level, as long as it visits the left child before the right child:

    • Each call knows its level. If result has no list for that level yet, it adds one.
    • It appends the node's value to result[level].
    • It visits the left subtree, then the right subtree, both with level + 1.

    Within one level, nodes are appended in the order the search reaches them. Because the left subtree is always fully explored before the right one, nodes further left are reached first, so every list ends up in left-to-right order. The zigzag is then a post-processing step: reverse the lists for levels 1, 3, 5 and so on.

    The search never skips a level on the way down. Reaching a node at level k means its parent at level k - 1 was visited, and that visit made sure a list existed for level k - 1. So at most one new list is ever needed at a time, when level equals result.len().

    Step by step

    1. Create result in zigzag_traversal as a mutable Vec<Vec<i32>> holding vec![], and call Self::dfs(&root, 0, &mut result). The tree is only read, so it is borrowed with &. The root is on level 0. result is filled in, so it is borrowed mutably.
    2. Reverse the odd levels: loop with level over (1..result.len()).step_by(2), which gives 1, 3, 5 and so on, and call reverse() on result[level].
    3. Return result as the last expression.
    4. Declare the helper dfs with the parameters root: &Option<Box<TreeNode>>, level: usize and result: &mut Vec<Vec<i32>>. The level is a usize because it is used as an index.
    5. Stop at missing nodes with if let Some(node) = root. Everything else goes inside this block.
    6. Add a list when the level is new: if level >= result.len(), push an empty vec![].
    7. Store the value: result[level].push(node.val).
    8. Recurse left with Self::dfs(&node.left, level + 1, result).
    9. Recurse right the same way with &node.right. Left must come before right.

    The solution in Rust

    impl Solution {
        fn dfs(root: &Option<Box<TreeNode>>, level: usize, result: &mut Vec<Vec<i32>>) {
            if let Some(node) = root {
                if level >= result.len() {
                    result.push(vec![]);
                }
                result[level].push(node.val);
                Self::dfs(&node.left, level + 1, result);
                Self::dfs(&node.right, level + 1, result);
            }
        }
    
        fn zigzag_traversal(root: Option<Box<TreeNode>>) -> Vec<Vec<i32>> {
            let mut result: Vec<Vec<i32>> = vec![];
            Self::dfs(&root, 0, &mut result);
            for level in (1..result.len()).step_by(2) {
                result[level].reverse();
            }
            result
        }
    }
    

    Rust details that matter here:

    • Borrowing instead of moving. dfs takes &Option<Box<TreeNode>>. In if let Some(node) = root, matching on a reference gives node as a reference too, so nothing is moved out of the tree. The recursive calls use &node.left and &node.right, which have the same reference type.
    • Passing result on. It is already a &mut Vec<Vec<i32>>, so recursive calls pass it as it is. Rust reborrows it for each call, and it is usable again afterwards.
    • vec![] and inference. The empty vector's type comes from the annotation on result, and result.push(vec![]) takes its element type from the vector it is pushed into.
    • step_by(2). (1..result.len()) is a range of usize, and step_by(2) visits every second value from its start. For an empty tree result.len() is 0, and 1..0 is an empty range, so the loop doesn't run. It doesn't panic.
    • Indexing. result[level] panics if level is out of bounds, which is why the list is added before the first access to it.
    • reverse() reverses the inner vector in place and returns nothing, so there is nothing to assign back.

    Complexity

    Time: O(n). The search visits each node once and does constant work per node. The reversals touch each value in an odd level at most once, so together they cost O(n) too.

    Space: O(n). The output holds all n values. The recursion adds O(h) call-stack depth, where h is the height of the tree, which never exceeds n.

    Edge cases and common mistakes

    • Empty tree. A None root does nothing in dfs, the reversal loop doesn't run, and the result is [].
    • Single node. The result is [[v]], and nothing is reversed.
    • Missing children. For root 1 with children 2 and 3, where 2 has only a left child 4 and 3 has only a right child 5, the answer is [[1], [3, 2], [4, 5]]. The lists contain only real nodes, and level 2 keeps 4 before 5 because it is read left to right.
    • Visiting right before left. Every list would come out in right-to-left order, so the reversal of the odd levels would leave the even levels backwards.
    • Reversing the wrong levels. The loop must start at 1. Starting at 0 would flip the even levels instead of the odd ones.
    • Forgetting to add a list. Without the check on level >= result.len(), the first node of each new level indexes past the end and panics.

    Testing

    • Root 3, children 9 and 20, 20's children 15 and 7: [[3], [20, 9], [15, 7]].
    • Root 1, children 2 and 3, 2's left child 4, 3's right child 5: [[1], [3, 2], [4, 5]].
    • A perfect tree with the values 1 to 15 in level order: [[1], [3, 2], [4, 5, 6, 7], [15, 14, 13, 12, 11, 10, 9, 8]].
    • An empty tree: [].
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free