Skip to content

Tree Traversal: Practice Problems with Solutions

Welcome to the practice problems for tree traversal. If you need a refresher on the code, the code templates have the recursive and iterative versions in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution. The solutions assume the TreeNode class from the template page, which is exactly what LeetCode provides in its editor.

Recommended Study Order

Start with Binary Tree Inorder Traversal to lock in the recursive skeleton, since every depth-first solution on this page is that skeleton with one line moved. Maximum Depth adds the postorder idea of combining subtree results. Level Order introduces the queue and the size snapshot, and Zigzag layers a direction toggle onto it. Validate BST shows why inorder is the default for anything involving BSTs. Finish with Maximum Path Sum and Serialize, which combine the traversals with genuinely tricky edge cases like negative values and null markers.

The order above is designed to build intuition progressively. The app schedules your reviews so you do not forget the Easy patterns while grinding Hard ones. Set up your review schedule .

Easy Problems

1. Binary Tree Inorder Traversal

LeetCode 94 | Difficulty: Easy

Brief: Return the inorder traversal of a binary tree’s node values.

Why this pattern: This is the fundamental recursive skeleton. Recurse left, process the node, recurse right. Nothing else happens.

Key Insight: For a BST the same function returns values in sorted order, which makes this the building block for validation and kth-smallest problems.

Visual:

    flowchart LR
    subgraph Tree["Input tree"]
        A((1)) --> B((2))
        A --> C((3))
    end
    subgraph Order["Visit order"]
        S1["1. Left subtree: 2"]
        S2["2. Root: 1"]
        S3["3. Right subtree: 3"]
    end
    Tree --> Order
  

Complexity: Time

O(N)
| Space
O(H)

Code:

var inorderTraversal = function(root) {
    const result = [];

    function inorder(node) {
        if (!node) return;

        // Left subtree, then the node, then the right
        // subtree. For a BST this is sorted order.
        inorder(node.left);
        result.push(node.val);
        inorder(node.right);
    }

    inorder(root);
    return result;
};

This is the main template applied directly. The base case returns without appending, so empty subtrees contribute nothing, and the recursive calls guarantee every node lands in the result exactly once. If you can write this function from memory, preorder and postorder are a one-line move away.

2. Maximum Depth of Binary Tree

LeetCode 104 | Difficulty: Easy

Brief: Return the maximum depth, defined as the number of nodes along the longest root-to-leaf path.

Why this pattern: The depth of a node is one plus the maximum depth of its children. The children must be answered before the parent, which is the postorder shape.

Key Insight: The base case is depth 0 for a null subtree. That single value propagates up through every level.

Visual:

    flowchart TD
    subgraph Tree["Tree with subtree depths"]
        A((3)) --> B((9))
        A --> C((20))
        C --> D((15))
        C --> E((7))
    end
    A -. "1 + max(1, 2)" .-> R["Max depth = 3"]
  

Complexity: Time

O(N)
| Space
O(H)

Code:

var maxDepth = function(root) {
    if (!root) return 0;

    const leftDepth = maxDepth(root.left);
    const rightDepth = maxDepth(root.right);

    // The node adds one level on top of the deeper
    // of its two subtrees.
    return 1 + Math.max(leftDepth, rightDepth);
};

The recursion returns a number instead of appending to a list, but the shape is the same postorder walk. Each node waits for both children, combines their answers with max, and adds one for itself. The space is O(H) because the call stack holds one frame per level.

Medium Problems

3. Binary Tree Level Order Traversal

LeetCode 102 | Difficulty: Medium

Brief: Return node values level by level, each level as its own list.

Why this pattern: Level-order is BFS on a tree. A queue holds the current frontier, and a size snapshot separates levels.

Key Insight: Children pushed during the loop belong to the next level. Snapshot the queue length once, before the inner loop.

Visual:

    flowchart TD
    subgraph Tree["Input tree"]
        A((3)) --> B((9))
        A --> C((20))
        C --> D((15))
        C --> E((7))
    end
    subgraph Output["Level lists"]
        L1["Level 0: [3]"]
        L2["Level 1: [9, 20]"]
        L3["Level 2: [15, 7]"]
    end
    Tree --> Output
  

Complexity: Time

O(N)
| Space
O(N)

Code:

var levelOrder = function(root) {
    if (!root) return [];

    const result = [];
    const queue = [root];

    while (queue.length) {
        // Snapshot the size. Children added during this
        // level belong to the next one.
        const levelSize = queue.length;
        const currentLevel = [];

        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            currentLevel.push(node.val);
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }

        result.push(currentLevel);
    }

    return result;
};

The levelSize snapshot is the whole trick. Without it the inner loop would absorb the next level as children get enqueued, and the output would come back as one flat list. The queue itself holds at most one full level, which is why the space is O(N) in the worst case.

4. Validate Binary Search Tree

LeetCode 98 | Difficulty: Medium

Brief: Determine whether a binary tree is a valid BST, meaning every left subtree holds smaller values and every right subtree holds larger ones.

Why this pattern: Inorder traversal of a valid BST produces strictly increasing values. Check each node against the previous one instead of building the whole list.

Key Insight: A node with value equal to the previous one is invalid, so the comparison must be strict. Storing the full traversal wastes space when one variable does the job.

Visual:

    flowchart TD
    subgraph Valid["Valid BST"]
        A1((5)) --> B1((3))
        A1 --> C1((7))
    end
    subgraph Invalid["Not a BST"]
        A2((5)) --> B2((6))
        A2 --> C2((7))
    end
    Valid --> V1["Inorder: 3, 5, 7 - increasing"]
    Invalid --> V2["Inorder: 6, 5, 7 - not increasing"]
  

Complexity: Time

O(N)
| Space
O(H)

Code:

var isValidBST = function(root) {
    let prev = -Infinity;

    function inorder(node) {
        if (!node) return true;

        // The left subtree must already be valid before
        // we compare the current node against the previous.
        if (!inorder(node.left)) return false;

        if (node.val <= prev) return false;
        prev = node.val;

        return inorder(node.right);
    }

    return inorder(root);
};

The previous value is initialized to negative infinity so the first node always passes. Java and C++ use the wider Long.MIN_VALUE and LLONG_MIN instead of the int versions, because the first node could legitimately be Integer.MIN_VALUE and the comparison would fail. The strict check is what rejects trees with duplicate values.

5. Binary Tree Zigzag Level Order Traversal

LeetCode 103 | Difficulty: Medium

Brief: Return level-order traversal where the direction alternates, left to right on even levels and right to left on odd ones.

Why this pattern: This is level-order with a direction flag. The BFS structure is unchanged.

Key Insight: Collect each level normally, then reverse every other level. No other change is needed.

Visual:

    flowchart TD
    subgraph Tree["Input tree"]
        A((3)) --> B((9))
        A --> C((20))
        C --> D((15))
        C --> E((7))
    end
    subgraph Output["Zigzag output"]
        L1["Level 0, left to right: [3]"]
        L2["Level 1, right to left: [20, 9]"]
        L3["Level 2, left to right: [15, 7]"]
    end
    Tree --> Output
  

Complexity: Time

O(N)
| Space
O(N)

Code:

var zigzagLevelOrder = function(root) {
    if (!root) return [];

    const result = [];
    const queue = [root];
    let leftToRight = true;

    while (queue.length) {
        const levelSize = queue.length;
        const currentLevel = [];

        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            currentLevel.push(node.val);
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }

        // Reverse odd levels so the direction alternates.
        if (!leftToRight) {
            currentLevel.reverse();
        }

        result.push(currentLevel);
        leftToRight = !leftToRight;
    }

    return result;
};

The reversal adds a constant amount of work per level, so the complexity is unchanged. The flag flips after every level, which means the root level is always left to right and the alternation is automatic from there.

Hard Problems

6. Binary Tree Maximum Path Sum

LeetCode 124 | Difficulty: Hard

Brief: Find the maximum sum along any path, where a path starts and ends at any nodes and follows parent-child edges.

Why this pattern: Postorder, because each node needs both subtree answers before deciding anything about itself.

Key Insight: A subtree with a negative contribution never helps any path through its parent, so negative contributions are clamped to 0. The global maximum may join the left and right branches through the current node, while the value returned upward can only use one branch.

Visual:

    flowchart TD
    A(("-10")) --> B((9))
    A --> C((20))
    C --> D((15))
    C --> E((7))
    A -. "best path" .-> P["15 -> 20 -> 7, sum 42"]
  

Complexity: Time

O(N)
| Space
O(H)

Code:

var maxPathSum = function(root) {
    let maxSum = -Infinity;

    function dfs(node) {
        if (!node) return 0;

        // Clamp negative contributions to 0. A subtree
        // that drags the sum down should not join any path.
        const left = Math.max(0, dfs(node.left));
        const right = Math.max(0, dfs(node.right));

        // A path may bend through this node, joining both
        // branches. Only the global maximum sees this.
        maxSum = Math.max(maxSum, node.val + left + right);

        // The parent can only use one branch, so return
        // the better side plus this node's value.
        return node.val + Math.max(left, right);
    }

    dfs(root);
    return maxSum;
};

Each node returns the best single-branch contribution it can offer its parent, which is its value plus the better of the two clamped child contributions. The global maximum is updated separately, because a path that bends through the node cannot continue upward. Clamping to 0 is what lets a negative subtree disappear instead of dragging every path down.

7. Serialize and Deserialize Binary Tree

LeetCode 297 | Difficulty: Hard

Brief: Encode a binary tree into a string and rebuild the exact same tree from that string.

Why this pattern: Preorder with explicit null markers captures the complete structure, because every node arrives before its children and every missing child is recorded.

Key Insight: Without null markers, a node with only a right child is indistinguishable from a node with only a left child. Mark every null child so the rebuild never guesses.

Visual:

    flowchart LR
    subgraph Tree["Binary tree"]
        A((1)) --> B((2))
        A --> C((3))
        C --> D((4))
        C --> E((5))
    end
    subgraph Encoded["Encoded string"]
        S1["1,2,null,null,3,4,null,null,5,null,null"]
    end
    Tree -->|serialize| Encoded
    Encoded -->|deserialize| Tree
  

Complexity: Time

O(N)
| Space
O(N)

Code:

var serialize = function(root) {
    const result = [];

    function dfs(node) {
        if (!node) {
            result.push('null');
            return;
        }
        result.push(String(node.val));
        dfs(node.left);
        dfs(node.right);
    }

    dfs(root);
    return result.join(',');
};

var deserialize = function(data) {
    const values = data.split(',');
    let index = 0;

    function dfs() {
        if (values[index] === 'null') {
            index++;
            return null;
        }

        const node = new TreeNode(parseInt(values[index]));
        index++;
        node.left = dfs();
        node.right = dfs();
        return node;
    }

    return dfs();
};

The serialize pass is preorder with “null” appended for missing children. Deserialize walks the same sequence in the same order, consuming one token for every node or null marker. The shared index guarantees the two passes mirror each other exactly. Space is O(N) because the string holds one token per node plus one per null child.

Hard problems are where patterns separate candidates who pass from candidates who get offers. Track your misses and review them on a spaced schedule so you do not repeat them in the real interview.

These seven problems cover both traversal families. Start with the recursive skeleton in Inorder and Max Depth, pick up the queue in Level Order and Zigzag, and finish with the postorder reasoning in Max Path Sum and the preorder encoding in Serialize. By the end, the question of which order a problem needs should answer itself in the first minute of any tree problem.

Done with these problems? The app has more, plus a review system that brings problems back right before you would forget them. Continue your prep .