Start learning free

LESSON 334 OF 414 · BINARY TREES

Inorder Traversal in Scala: Recursive DFS on a Binary Tree

Practice O(n) Read the full explanation

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

Example:

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

Output: [1, 3, 2]

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

STEP 1 OF 8
    Ctrl + Enter to check

    SCALA · BINARY TREES

    Inorder Traversal in Scala, explained

    Problem overview

    The method inorderTraversal receives the root of a binary tree and returns the node values as an Array[Int] in inorder: the whole left subtree first, then the node itself, then the whole right subtree. The rule applies at every node, not only at the root.

    In the example, 1 has no left child and a right child 2, and 2 has a left child 3. Starting at 1, the left subtree is empty, so 1 comes first. The right subtree has root 2, whose left subtree is just 3, so the order continues with 3, then 2. The result is [1, 3, 2].

    The statement adds a follow-up: the recursive solution is easy, so try an iterative one. This lesson writes the recursive version.

    Approach: let recursion follow the tree's shape

    A tree is a node with two smaller trees below it, and an empty tree is represented by null. That makes the traversal naturally recursive. To list a tree inorder:

    1. If the tree is empty, add nothing.
    2. List the left subtree.
    3. Add this node's value.
    4. List the right subtree.

    Only the position of step 3 distinguishes the three classic depth-first orders. Adding the value before both recursive calls gives preorder, and adding it after both gives postorder. Putting it between them gives inorder. For a binary search tree, inorder visits the values in ascending order, which is the main reason the order is worth knowing.

    The helper does not return a list for each subtree. Instead, one shared buffer is passed down every call, and each call appends to it. That avoids building and joining many small lists.

    Step by step

    1. Declare the helper dfs above inorderTraversal. It takes the subtree's root and the shared buffer result, and returns Unit because its work shows up in the buffer. The type is written with the full path scala.collection.mutable.ArrayBuffer[Int], so no import is needed.
    2. Stop at an empty subtree. If root is null, return immediately. This is the base case that ends every branch of the recursion.
    3. Visit the left subtree by calling dfs(root.left, result). Everything in it is appended before the current node.
    4. Append the node's value with result += root.value. The lesson's TreeNode keeps its number in the field value.
    5. Visit the right subtree with dfs(root.right, result).
    6. In inorderTraversal, create result as an empty ArrayBuffer[Int] with val.
    7. Call dfs(root, result) to fill the buffer.
    8. Return result.toArray to turn the buffer into the Array[Int] the method promises.

    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)
            result += root.value
            dfs(root.right, result)
        }
    
        def inorderTraversal(root: TreeNode): Array[Int] = {
            val result = scala.collection.mutable.ArrayBuffer[Int]()
            dfs(root, result)
            result.toArray
        }
    }
    

    The Scala details worth a closer look:

    • Buffers grow, arrays don't. A Scala Array has a fixed size, so the traversal collects values in a growable ArrayBuffer and converts it once at the end with toArray.
    • val fixes the name, not the contents. result is a val, but result += root.value still changes the buffer, because += on a mutable buffer appends in place.
    • The buffer is shared. dfs receives a reference to the same buffer in every call, so values appended by deep calls are visible to the caller once they return.
    • The final expression is the result. In inorderTraversal, result.toArray is the last line, so no return is needed. In dfs, return is only there for the early exit. Writing if (root != null) { ... } around the three lines would avoid it.
    • null marks a missing child. This lesson's TreeNode uses null for an absent left or right child, so comparing with == against null is how the base case recognises an empty subtree.

    Complexity

    Time: O(n) for n nodes. Each node triggers one real call of dfs, plus one more call for each missing child, and the total number of calls is at most 2n + 1. Appending to the buffer is constant time on average.

    Space: O(n) for the result, and O(h) extra for the call stack, where h is the height of the tree. A balanced tree has a height around log n, while a tree that is one long chain has a height of n.

    Edge cases and common mistakes

    • Empty tree. With a null root, dfs returns at once and the method returns an empty array.
    • Single node. Both subtrees are empty, so the result is just the node's value.
    • Wrong position of the append. Putting result += root.value before the left call gives preorder, and putting it after the right call gives postorder. For the example, those would give [1, 2, 3] and [3, 2, 1].
    • Forgetting the base case. Without the null check, the first leaf's missing child causes a NullPointerException on root.left.
    • Very deep trees. Each level of the tree uses one stack frame. A left chain of a few thousand nodes is fine, but a chain of hundreds of thousands can overflow the stack with a StackOverflowError; the exact depth depends on the JVM's stack size. That is the practical reason for the iterative follow-up, where an explicit stack of nodes replaces the call stack.
    • Creating a new buffer in each call. If dfs made its own buffer and returned it, the code would have to concatenate three pieces at every node. Passing one shared buffer is simpler and faster.

    Testing

    • Root 1 with a right child 2 whose left child is 3 gives [1, 3, 2].
    • A null root gives [].
    • Root 5 with left child 3 (children 1 and 4) and right child 8 gives [1, 3, 4, 5, 8]. This is a binary search tree, so the output is sorted.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free