Start learning free

LESSON 327 OF 414 · BINARY TREES

Post Order Tree Traversal in Rust: Recursive DFS

Practice O(n) Read the full explanation

Given a binary tree, return the postorder traversal of its nodes' values.

Example:

Input: [1,None,2,3]
   1
    \
     2
    /
   3

Output: [3,2,1]

Follow up: Recursive solution is trivial, could you do it iteratively?

STEP 1 OF 6
    Ctrl + Enter to check

    RUST · BINARY TREES

    Post Order Tree Traversal in Rust, explained

    Problem overview

    The function post_order_traversal receives the root of a binary tree and returns the node values in post order: the whole left subtree, then the whole right subtree, and the node itself last. The root is an Option<Box<TreeNode>>, which is None for an empty tree. A node has a val and two children, left and right, of the same Option<Box<TreeNode>> type.

    For the tree with root 1, no left child, and a right child 2 that has a left child 3, the answer is [3, 2, 1]. Node 3 comes first because it is the only thing in 2's left subtree, then 2 itself, then the root. For the tree with root 1, children 2 and 3, and 2's children 4 and 5, the answer is [4, 5, 2, 3, 1].

    Approach: recursion mirrors the definition

    The order is defined recursively, so the code can follow the definition directly. To list a subtree in post order:

    1. list its left subtree;
    2. list its right subtree;
    3. list the node's own value.

    An empty subtree lists nothing, which is the base case that ends the recursion. Putting the node's value after both recursive calls is what makes it post order. Putting it before them would give pre order, and putting it between them would give in order.

    Instead of returning a new vector from every call and joining them, one vector is shared. The helper receives a mutable reference to it and pushes values as it goes. Everything is appended in the right order without any copying of partial results.

    The lesson has a second solution for the same problem, which replaces the recursion with an explicit stack. This one is the shortest and most direct.

    Step by step

    1. Create the output in post_order_traversal: let mut result = Vec::new();. Rust works out the element type, i32, from how it is used.
    2. Call the helper with Self::dfs(root, &mut result) and end the function with result, without a semicolon, to return it. Passing &mut result lets the helper push into the vector while this function keeps ownership.
    3. Declare dfs with the parameters root: Option<Box<TreeNode>> and result: &mut Vec<i32>. It returns nothing.
    4. Handle the empty case with if let Some(node) = root. The block runs only when there is a node. For None nothing happens, so the recursion stops at missing children without separate checks.
    5. Recurse left with Self::dfs(node.left, result).
    6. Recurse right with Self::dfs(node.right, result).
    7. Record the node by pushing node.val, after both recursive calls.

    The solution in Rust

    impl Solution {
        fn post_order_traversal(root: Option<Box<TreeNode>>) -> Vec<i32> {
            let mut result = Vec::new();
            Self::dfs(root, &mut result);
            result
        }
    
        fn dfs(root: Option<Box<TreeNode>>, result: &mut Vec<i32>) {
            if let Some(node) = root {
                Self::dfs(node.left, result);
                Self::dfs(node.right, result);
                result.push(node.val);
            }
        }
    }
    

    Rust details that matter here:

    • The helper takes the tree by value. root is an owned Option<Box<TreeNode>>, so if let Some(node) = root moves the box into node. The tree is consumed: after the call, the caller no longer has it. That is fine for a function that is handed the only copy of the tree.
    • Moving the children out of the box. node.left and node.right are moved into the recursive calls, which is allowed for the fields of a Box. node.val is an i32, which is Copy, so it can still be read after the two moves. That is why result.push(node.val) compiles on the last line.
    • Passing result along. The parameter is already a &mut Vec<i32>. Writing Self::dfs(node.left, result) reborrows it for the duration of the call, and then result is usable again for the next call and for the push. No &mut is added at those call sites.
    • Self::dfs. The helper has no self parameter, so it is an associated function, called through the type. Self means Solution inside the impl block.
    • Tail expression. result on its own line, without a semicolon, is the function's return value.

    Complexity

    Time: O(n). Each node is visited exactly once, and each visit does one push. Missing children are visited too, but there are at most n + 1 of them.

    Space: O(h) extra, plus the O(n) output. The call stack is as deep as the tree is tall, h. For a balanced tree that is about log n, and for a tree where every node has one child it is n.

    Edge cases and common mistakes

    • Empty tree. A None root skips the if let block and the function returns an empty vector, [].
    • A single node. Both recursive calls get None, so the result is just that node's value.
    • Wrong position of the push. Moving result.push(node.val) above the recursive calls gives pre order. For the tree with root 1, children 2 and 3, and 2's children 4 and 5, that would print [1, 2, 4, 5, 3] instead of [4, 5, 2, 3, 1].
    • Visiting right before left. That would produce a mirror order. Post order is defined with the left subtree first.
    • Using the tree after the call. Because dfs takes ownership, trying to use root again after Self::dfs(root, &mut result) is a compile error about a moved value. A version that borrows the tree with &Option<Box<TreeNode>> would keep it usable.
    • Very deep trees. Recursion depth equals the height of the tree, so a very long chain uses a lot of stack. The lesson's second solution uses an explicit stack to avoid that.

    Testing

    • Root 1, right child 2, and 2's left child 3: [3, 2, 1].
    • Root 1, children 2 and 3, 2's children 4 and 5: [4, 5, 2, 3, 1].
    • 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