Start learning free

LESSON 326 OF 414 · BINARY TREES

Post Order Tree Traversal in Scala: Recursive DFS

Practice O(n) Read the full explanation

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

Example:

Input: [1,null,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

    SCALA · BINARY TREES

    Post Order Tree Traversal in Scala, explained

    Problem overview

    The method postOrderTraversal receives the root of a binary tree and returns the values of its nodes in post order as an Array[Int]. Post order visits the left subtree first, then the right subtree, and the node itself last.

    For the tree whose root holds 1, with no left child and a right child 2 that has a left child 3, the result is Array(3, 2, 1). Node 3 is the only thing in the left subtree of 2, so it comes first. Then comes 2, whose subtrees are done, and the root 1 comes last. A bigger example: for the root 1 with the left child 2 (children 4 and 5) and the right child 3 (right child 6), the result is Array(4, 5, 2, 6, 3, 1).

    Approach: recursion that records the node last

    A tree is a node with two smaller trees under it, or nothing at all. That shape suggests recursion. To list a subtree in post order, list its left subtree, list its right subtree, then add the node's own value. A missing child is an empty subtree and contributes nothing, so a null check ends the recursion.

    The recursive helper dfs(root, result) doesn't return a list. It appends to one shared ArrayBuffer, which the main method creates, passes down and converts at the end. This avoids building and concatenating many small lists.

    Only the position of the "record the value" line differs between the three classic orders: before the recursive calls it gives pre order, between them in order, after them post order. A second lesson on this problem solves it without recursion, using an explicit stack.

    Step by step

    1. In postOrderTraversal, create val result as an empty ArrayBuffer[Int], call dfs(root, result) to fill it, and end with result.toArray, because the method returns an Array[Int].
    2. Declare the helper dfs with the parameters root: TreeNode and result: ArrayBuffer[Int]. It returns Unit: its output is what it adds to the buffer.
    3. Stop at an empty subtree. If root == null, return at once. A missing child is null in the lesson's TreeNode class.
    4. Visit the left subtree with dfs(root.left, result). No check on root.left is needed, because the first line of the helper handles null.
    5. Visit the right subtree with dfs(root.right, result).
    6. Record the node with result += root.value, after both calls. This position is what makes the order post order.

    The solution in Scala

    class Solution {
        def dfs(root: TreeNode, result: scala.collection.mutable.ArrayBuffer[Int]): Unit = {
            if (root == null) {
                return
            }
            dfs(root.left, result)
            dfs(root.right, result)
            result += root.value
        }
    
        def postOrderTraversal(root: TreeNode): Array[Int] = {
            val result = scala.collection.mutable.ArrayBuffer[Int]()
            dfs(root, result)
            result.toArray
        }
    }
    

    Scala details:

    • ArrayBuffer versus Array. An Array has a fixed size, while an ArrayBuffer grows as values are appended with +=. The method must return an Array[Int], so toArray makes the final copy.
    • The buffer is shared. Every recursive call receives the same object, so appending in a deep call is visible to the caller. That is why dfs can return Unit.
    • A bare return in a method that returns Unit simply ends the call. It sits at the method's own level here, not inside a closure, so it is an ordinary return.
    • The last expression is the result. result.toArray needs no return.
    • Comparing with null. root == null is a safe comparison in Scala: == compares values and handles null without throwing.
    • Printing the result. println of an Array shows an object reference, not the values. Use mkString(", ") to look at them.

    Complexity

    Time: O(n). Each node is visited once, and each visit does a constant amount of work. The calls on null children add at most n + 1 more constant-time calls.

    Space: O(h) for the recursion plus O(n) for the result, where h is the height of the tree. The call stack is as deep as the longest path from the root. For a balanced tree h is about log n, and for a tree that is a single chain it is n. The JVM's stack is limited: a chain of a few thousand nodes is fine, but a very deep one, such as 100,000 nodes, typically ends in a StackOverflowError; the exact limit depends on the stack size. The iterative version in the other lesson avoids that limit.

    Edge cases and common mistakes

    • Empty tree. With root = null, the helper returns at once and the method returns an empty array.
    • Single node. The result is that node's value alone.
    • Only one child. A node with only a right child still works: the left call hits null and returns, and the right subtree comes next.
    • Recording too early. Moving result += root.value above the two calls gives pre order. For the first example that would be 1, 2, 3 instead of 3, 2, 1.
    • Creating a new buffer in each call. If the helper made its own buffer, the values would stay in the callers' separate buffers and the final result would be empty or incomplete.
    • Using the wrong field name. A node's number is read with root.value in this lesson.

    Testing

    • Root 1, right child 2, whose left child is 3: Array(3, 2, 1).
    • Root 1 with left child 2 (children 4, 5) and right child 3 (right child 6): Array(4, 5, 2, 6, 3, 1).
    • null: an empty array.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free