Skip to main content

Tree Traversals: Preorder, Inorder, and Postorder Depth-First Search

Complete Java implementation of tree traversal algorithms with iterative and recursive approaches, including time/space complexity analysis and practical use cases.

Tree traversals are fundamental algorithms for visiting all nodes in a tree data structure, with preorder, inorder, and postorder representing different visitation orders that serve distinct computational purposes in applications like expression parsing, serialization, and DOM manipulation.

1. Pattern Restatement

Clarifying questions to ask the interviewer:

  1. Are we guaranteed that the input is a valid binary tree, or should we handle null/invalid inputs?
  2. What is the expected return format - should we return a list of values, or do we need to process nodes in-place?
  3. Can we modify the input tree structure during traversal, or must we preserve it?
  4. What are the constraints on the number of nodes (e.g., max size, negative values)?
  5. Should we aim for O(n) time complexity or are there specific performance targets?

Restate the pattern or problem type in your own words: This is a fundamental tree traversal pattern where we visit all nodes in a binary tree following specific orders: pre-order (root, left, right), in-order (left, root, right), or post-order (left, right, root). The pattern involves systematically visiting each node exactly once while maintaining a specific processing order.

Clarify the signal words and constraint shape that point to it: Signal words include "tree traversal", "preorder", "inorder", "postorder", "binary tree", "visit all nodes", and "process in order". The constraint shape typically involves a hierarchical data structure where each node has at most two children, and we need to process nodes in a specific sequence.

Note any assumptions you are making: I'm assuming we're working with a standard binary tree structure where each node has left and right child pointers (or null). I'm also assuming that the tree is valid and not a general n-ary tree. The traversal order should be strictly maintained as specified.

Identify whether the pattern is best suited for static, streaming, or online scenarios if relevant: This pattern is primarily suited for static scenarios where the entire tree structure is available upfront. While it can be adapted for streaming in some cases (e.g., processing nodes as they arrive), the standard tree traversal pattern is designed for complete static structures where all nodes are accessible. It's not typically used in real-time streaming contexts where nodes arrive dynamically.

The pattern works best when we have a complete binary tree structure and need to process all nodes in a specific hierarchical order, making it ideal for problems like expression tree evaluation, tree serialization/deserialization, and various tree-based algorithmic problems.

2. When to Use This Pattern

Signal words and constraint shapes:

  • "Traverse" or "visit all nodes" in a tree structure
  • "Preorder", "inorder", or "postorder" traversal requirements
  • Problems asking for specific node ordering or tree property validation
  • "Print all paths" or "find nodes with specific properties" in tree structures

Why this pattern fits better than alternatives: Tree traversals are the fundamental operation for processing tree data structures. Unlike graph traversal (which requires handling cycles and visited nodes), tree traversals are deterministic and naturally follow parent-child relationships. The three traversal orders (preorder, inorder, postorder) each provide different perspectives on the tree structure:

  • Preorder: Root-first processing (useful for copying/serializing trees)
  • Inorder: Left-root-right (ideal for BSTs where we want sorted order)
  • Postorder: Left-right-root (useful for deleting nodes or calculating sizes)

Conditions where variations apply:

  • For binary trees with specific constraints (like BST validation), inorder traversal is preferred
  • For expression trees, postorder traversal is optimal for evaluating expressions
  • For level-order processing, BFS with queue is more appropriate than any traversal order

Core insight: The key insight is that tree traversals are not just about visiting nodes, but about systematically exploring the hierarchical structure. Each traversal order represents a specific way of organizing information flow through the tree, making it possible to solve complex problems like validating BSTs or reconstructing trees from traversal sequences by leveraging the inherent ordering properties.

This pattern is particularly powerful because it provides a systematic way to process tree data without requiring additional state tracking or complex algorithms, making it ideal for problems that require examining every node in a predictable sequence.

3. Approach Evolution

Interview communication script:

My first instinct is to solve this with recursion because tree traversals are naturally recursive - we visit a node, then recursively process its children. But that's O(n²) in worst case for skewed trees due to call stack overhead, and we're not even using the fact that we can process nodes in linear time. Let me think about how to optimize this.

I notice that if I maintain a stack and process nodes iteratively, I can reduce it to O(n) time complexity by avoiding the function call overhead. The key insight is that we can simulate the recursion stack manually and process nodes in exactly the order required by the traversal type.

The core optimization is to use an explicit stack to simulate the recursive call stack, allowing us to control exactly when we process nodes and how we manage state. For pre-order traversal, we want to process the current node before its children, so we push right child first then left child. For in-order, we want to process left subtree, current node, then right subtree - so we push the rightmost path onto stack and process nodes in the right order.

Let me trace through this more carefully to make sure I'm handling all cases correctly.

Brute-force approach and why it is not ideal:

The most straightforward approach is recursive tree traversal. For each node, we recursively process its left subtree, then the current node, then its right subtree. While this is intuitive and easy to understand, it has several drawbacks:

  1. Stack overflow risk: For very deep or skewed trees, the recursion depth can exceed system limits
  2. Function call overhead: Each recursive call adds to stack space and has function call cost
  3. Memory inefficiency: The implicit call stack can be larger than necessary

Improved approach and the key optimization:

The improved iterative approach uses an explicit stack to simulate the recursive behavior. The key insight is that we can maintain the same traversal order by carefully managing what we push to and pop from our stack:

  • For pre-order: Process node, push right child, then left child (so left is processed first)
  • For in-order: Push all leftmost nodes to stack, process when we can't go left anymore
  • For post-order: Use two-stack technique or modify the order to process nodes in reverse

The optimization is that we eliminate function call overhead and have explicit control over our traversal state, giving us O(n) time with O(h) space where h is the height of the tree.

Final approach with step-by-step reasoning:

I'll implement the iterative pre-order traversal as my primary solution:

  1. Start with root node in stack
  2. While stack is not empty:
    • Pop current node from stack
    • Process the node (add to result)
    • Push right child first, then left child (so left is processed first on next iteration)
  3. Continue until stack is empty

This works because:

  • Pre-order visits nodes in "root-left-right" order
  • By pushing right child before left child, we ensure left is processed first on the next iteration
  • The stack naturally maintains the correct processing order

Important invariants that must hold:

  1. Stack always contains nodes that need to be processed
  2. When we pop a node, it's guaranteed to be processed exactly once
  3. For pre-order: Right child is pushed before left child so left subtree is processed first
  4. Stack never contains null nodes (we check before pushing)
  5. Processed nodes are never re-added to stack

The iterative approach is more efficient than recursion for large trees and provides better control over the traversal process.

4. Data Structure Choice

For tree traversal problems, the primary data structures needed are:

1. TreeNode Class

  • Why needed: Represents the basic structure of a binary tree node with left and right children
  • Time/Space: O(1) per node for storage and access
  • Implementation: Standard class with value, left, and right pointers

2. Stack (for iterative approaches)

  • Why needed: Simulates the function call stack for recursive traversals
  • Time/Space: O(h) where h is tree height; worst case O(n) for skewed trees
  • Key benefit: Enables iterative solutions that avoid stack overflow issues

3. List/ArrayList

  • Why needed: Stores the traversal results in the required order
  • Time/Space: O(n) for storing all n nodes
  • Implementation: Simple collection to accumulate results

4. Deque (for level-order traversal)

  • Why needed: Efficient insertion at both ends for breadth-first traversal
  • Time/Space: O(w) where w is maximum width of tree
  • Alternative: Queue can also be used but Deque offers more flexibility

The stack is the most critical data structure for iterative traversal solutions, as it directly enables the conversion from recursive to iterative approaches while maintaining the same logical order of processing.

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

5. Algorithm Walkthrough

Let me trace through the iterative postorder traversal algorithm using the example tree:

    1
   / \
  2   3
 / \
4   5

Step-by-step execution:

  1. Initialization:

    • Stack = [1] (root node)
    • LastVisited = null
  2. First iteration:

    • Pop 1 from stack → current = 1
    • Since 1 has children (2,3), we push right child first, then left child
    • Stack becomes [3, 2]
    • We don't process 1 yet because we need to handle its children first
  3. Second iteration:

    • Pop 2 from stack → current = 2
    • Push right child (5), then left child (4)
    • Stack = [3, 5, 4]
    • Process 2's children before 2 itself
  4. Third iteration:

    • Pop 4 from stack → current = 4
    • 4 is a leaf node, no children to push
    • Stack = [3, 5]
    • Since 4 is a leaf, we can safely add it to result
    • Result = [4]
  5. Fourth iteration:

    • Pop 5 from stack → current = 5
    • 5 is a leaf node, no children to push
    • Stack = [3]
    • Add 5 to result
    • Result = [4, 5]
  6. Fifth iteration:

    • Pop 3 from stack → current = 3
    • 3 has no children to push, but we need to process it after its children
    • Stack = []
    • Since 3 has no children, we can process it now
    • Result = [4, 5, 3]
  7. Sixth iteration:

    • Stack is empty, so we process node 1
    • Since 1 has been processed and its children were already handled, we add it to result
    • Result = [4, 5, 3, 1]

Wait, let me reconsider the correct approach:

Corrected walkthrough with proper postorder logic:

  1. Initialization:

    • Stack = [1]
    • LastVisited = null
  2. First iteration:

    • Pop 1 → current = 1
    • Push children in reverse order (right then left): [3, 2]
    • Mark 1 as visited in a way that we know its children are processed
    • We continue processing to get to the leaf nodes first
  3. Second iteration:

    • Pop 2 → current = 2
    • Push [5, 4] to stack
    • Continue traversal down to leaves
  4. Third iteration:

    • Pop 4 → current = 4
    • No children, so we can safely add to result
    • Result = [4]
  5. Fourth iteration:

    • Pop 5 → current = 5
    • No children, add to result
    • Result = [4, 5]
  6. Fifth iteration:

    • Pop 3 → current = 3
    • No children, add to result
    • Result = [4, 5, 3]
  7. Sixth iteration:

    • Pop 1 → current = 1
    • All children processed, add to result
    • Final Result = [4, 5, 3, 1]

The key insight is that we process nodes in reverse postorder and use a stack to simulate the recursive call stack, ensuring we handle children before parents.

Edge cases handled:

  • Empty tree returns empty list
  • Single node tree returns that node
  • Skewed trees work correctly through the stack-based approach

6. Java Solution

import java.util.*;

// Definition for a binary tree node
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

public class TreeTraversal {
    
    // Recursive pre-order traversal
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        preorderHelper(root, result);
        return result;
    }
    
    private void preorderHelper(TreeNode node, List<Integer> result) {
        if (node == null) return;
        
        result.add(node.val);           // Visit root
        preorderHelper(node.left, result);   // Traverse left subtree
        preorderHelper(node.right, result);  // Traverse right subtree
    }
    
    // Iterative pre-order traversal using stack
    public List<Integer> preorderTraversalIterative(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) return result;
        
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            result.add(node.val);
            
            // Push right first, then left (so left is processed first)
            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left != null) {
                stack.push(node.left);
            }
        }
        
        return result;
    }
    
    // Recursive in-order traversal
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        inorderHelper(root, result);
        return result;
    }
    
    private void inorderHelper(TreeNode node, List<Integer> result) {
        if (node == null) return;
        
        inorderHelper(node.left, result);    // Traverse left subtree
        result.add(node.val);                // Visit root
        inorderHelper(node.right, result);   // Traverse right subtree
    }
    
    // Iterative in-order traversal using stack
    public List<Integer> inorderTraversalIterative(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        TreeNode current = root;
        
        while (current != null || !stack.isEmpty()) {
            // Go to the leftmost node
            while (current != null) {
                stack.push(current);
                current = current.left;
            }
            
            // Process the node
            current = stack.pop();
            result.add(current.val);
            
            // Move to right subtree
            current = current.right;
        }
        
        return result;
    }
    
    // Recursive post-order traversal
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        postorderHelper(root, result);
        return result;
    }
    
    private void postorderHelper(TreeNode node, List<Integer> result) {
        if (node == null) return;
        
        postorderHelper(node.left, result);   // Traverse left subtree
        postorderHelper(node.right, result);  // Traverse right subtree
        result.add(node.val);                 // Visit root
    }
    
    // Iterative post-order traversal using two stacks
    public List<Integer> postorderTraversalIterative(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) return result;
        
        Stack<TreeNode> stack1 = new Stack<>();
        Stack<TreeNode> stack2 = new Stack<>();
        
        stack1.push(root);
        
        // First stack to get the reverse post-order
        while (!stack1.isEmpty()) {
            TreeNode node = stack1.pop();
            stack2.push(node);
            
            if (node.left != null) {
                stack1.push(node.left);
            }
            if (node.right != null) {
                stack1.push(node.right);
            }
        }
        
        // Second stack gives us the actual post-order
        while (!stack2.isEmpty()) {
            result.add(stack2.pop().val);
        }
        
        return result;
    }
}

This solution provides both recursive and iterative implementations for all three tree traversal types:

  • Pre-order: Root → Left → Right
  • In-order: Left → Root → Right
  • Post-order: Left → Right → Root

The iterative approaches use explicit stacks to simulate the call stack behavior of recursion. For post-order, I use a two-stack approach which is more intuitive than the single-stack reversal technique. All implementations handle null nodes gracefully and maintain proper traversal order.

7. Complexity Analysis

Time Complexity: O(n) where n is the number of nodes in the binary tree. Each node is visited exactly once during the traversal, and the work done at each node (pushing/popping from stack, processing values) takes constant time.

Space Complexity: O(h) where h is the height of the tree. In the worst case (skewed tree), the space complexity becomes O(n) as the stack can contain all nodes. In a balanced binary tree, the space complexity is O(log n).

The bounds are correct because:

  • We process each node exactly once, giving us the linear time complexity
  • The maximum stack depth corresponds to the maximum recursion depth or the maximum number of nodes in the traversal path, which is bounded by the tree height
  • For a complete binary tree, the height is log n, making space complexity O(log n)
  • For a skewed tree (essentially a linked list), the height is n, making space complexity O(n)

This solution is optimal for the iterative approach since we must visit each node at least once to produce a valid traversal. The iterative solution matches the time complexity of the recursive approach, and the space complexity is bounded by the tree's height, which is the best possible for iterative implementations of tree traversals.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty tree: Handle null root gracefully to avoid null pointer exceptions
  • Single node: Verify that leaf nodes are processed correctly in all traversal orders
  • Skewed trees: Deep left/right-skewed trees can cause stack overflow in recursive approaches

Duplicate Values and Repeated States

  • Duplicate values: Tree traversal algorithms don't distinguish between duplicate values, so the traversal sequence remains consistent regardless of value duplication
  • Same structure with different values: Traversal orders will be identical for structurally equivalent trees with different node values

Boundary Conditions

  • Null nodes: Properly handle null left/right children in traversal logic
  • Single child trees: Ensure proper handling when a node has only left or only right child
  • Maximum recursion depth: For very deep trees, consider iterative approaches to avoid stack overflow

Common Implementation Mistakes

  • Incorrect order: Confusing pre-order (root → left → right) vs post-order (left → right → root) sequences
  • Missing base case: For recursive implementations, forgetting to check for null nodes can cause runtime exceptions
  • Incorrect stack usage: In iterative approaches, pushing the wrong order of nodes to the stack can change traversal sequence
  • State persistence: Not properly managing state variables between recursive calls in global or static contexts

Implementation-Specific Pitfalls

  • Iterator vs List: When returning results as lists, ensure proper collection initialization and handling of empty collections
  • Thread safety: If using shared state in concurrent scenarios, ensure thread-safe access patterns
  • Memory leaks: In iterative approaches with custom node wrappers, ensure proper cleanup of temporary objects

9. Alternative Approaches

There are several alternative approaches to tree traversals, each with different trade-offs:

1. Recursive Approach: The most intuitive solution uses recursion to traverse the tree. While clean and readable, it has O(h) space complexity due to call stack, where h is the tree height. For very deep trees, this could lead to stack overflow.

2. Iterative with Explicit Stack: Instead of recursion, we can simulate the call stack using an explicit stack data structure. This approach gives us full control over traversal order and avoids recursion limits, but requires more complex bookkeeping to track the current node and its state.

3. Morris Traversal: This is a space-optimized approach that uses threading to traverse the tree without recursion or explicit stack. It temporarily modifies the tree structure by creating links to inorder predecessors, then restores the original structure. This achieves O(1) space complexity but is more complex to implement correctly and modifies the tree during traversal.

4. Threaded Binary Tree: If we're allowed to modify the tree structure, we can augment nodes with "thread" pointers that allow us to traverse without stacks or recursion. This is particularly useful in systems where we perform many traversals on the same tree.

The chosen solution using explicit stack is preferred because it's clean, readable, and has predictable space complexity that matches the recursion depth. It's also easier to debug and maintain than Morris traversal, which can be error-prone due to its complex pointer manipulation.

The iterative stack approach strikes the best balance between simplicity, performance, and reliability for most practical applications.

10. Test Cases

// Test case 1: Basic binary tree with multiple levels
//       1
//      / \
//     2   3
//    / \
//   4   5
TreeNode root1 = new TreeNode(1);
root1.left = new TreeNode(2);
root1.right = new TreeNode(3);
root1.left.left = new TreeNode(4);
root1.left.right = new TreeNode(5);

// Test case 2: Single node tree
TreeNode root2 = new TreeNode(1);

// Test case 3: Empty tree
TreeNode root3 = null;

// Test case 4: Skewed tree (all left children)
//   1
//    \
//     2
//      \
//       3
TreeNode root4 = new TreeNode(1);
root4.right = new TreeNode(2);
root4.right.right = new TreeNode(3);

// Test case 5: Complete binary tree
//       1
//      / \
//     2   3
//    / \ / \
//   4  5 6  7
TreeNode root5 = new TreeNode(1);
root5.left = new TreeNode(2);
root5.right = new TreeNode(3);
root5.left.left = new TreeNode(4);
root5.left.right = new TreeNode(5);
root5.right.left = new TreeNode(6);
root5.right.right = new TreeNode(7);

The test cases cover:

  • A typical balanced binary tree to verify all traversal orders work correctly
  • Edge case of single node (boundary condition)
  • Empty tree scenario (null input handling)
  • Skewed tree to test edge case where one side is completely missing
  • Complete binary tree to verify proper handling of full nodes

Each test case should produce predictable output sequences that can be validated against expected pre-order, in-order, and post-order traversals. The edge cases ensure proper handling of null inputs and boundary conditions that commonly cause bugs in tree traversal implementations.

11. Final Summary

The core idea is to leverage iterative tree traversal using explicit stacks to avoid recursion overhead while maintaining the same logical order as recursive approaches. This solution works by simulating the call stack behavior through manual stack management, ensuring that nodes are processed in the correct pre-order sequence (root → left → right) without modifying the original tree structure. The efficiency comes from O(n) time complexity with O(h) space usage where h is the tree height, making it optimal for scenarios where recursion stack limitations or performance concerns exist.

Was this article helpful?

Topics in this article

More from DSA Patterns

  • DSA Problems

    Lowest Common Ancestor of a BST

    Find the lowest common ancestor in a binary search tree using iterative and recursive approaches with O(log n) time complexity.

  • DSA Problems

    Subtree of Another Tree Java Solution

    Complete Java implementation for checking if one binary tree is a subtree of another, including multiple approaches and time complexity analysis.

  • DSA Problems

    Same Tree and Symmetric Tree Problems in Java

    Comprehensive Java solution for same tree and symmetric tree problems including recursive and iterative approaches with time and space complexity analysis.

  • DSA Problems

    Path Sum II Java Solution with DFS and Backtracking

    Complete Java implementation for Path Sum II problem with detailed explanation of DFS approach and backtracking techniques for finding all root-to-leaf paths with target sum.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.