Facebook Pixel

107. Binary Tree Level Order Traversal II

Problem Description

You are given the root of a binary tree. Your task is to return the values of all nodes organized by levels, but in bottom-up order. This means you should traverse the tree level by level from left to right, but the final result should display the deepest level first and the root level last.

For example, if you have a binary tree like this:

      3
     / \
    9   20
        / \
       15  7

A regular level-order traversal would give you: [[3], [9, 20], [15, 7]]

But for this problem, you need to return the bottom-up version: [[15, 7], [9, 20], [3]]

The solution uses a BFS (Breadth-First Search) approach with a queue. It processes the tree level by level from top to bottom, collecting all nodes at each level into a list. Each level's values are stored in order from left to right. After collecting all levels in the normal top-down order, the final step reverses the entire result using ans[::-1] to achieve the bottom-up ordering required by the problem.

The key steps in the algorithm are:

  1. Start with the root node in a queue
  2. For each level, process all nodes currently in the queue
  3. Collect their values and add their children to the queue for the next level
  4. Continue until all levels are processed
  5. Reverse the final list of levels to get bottom-up order

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph (specifically, it's a connected acyclic graph).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree.

DFS?

  • Not necessarily: While DFS could work for tree traversal, the problem specifically asks for level-by-level traversal (level order), which is a characteristic pattern of BFS. The requirement to process nodes level by level from left to right strongly suggests BFS over DFS.

Let's reconsider the path through the flowchart:

Is it a graph?

  • Yes: Binary trees are graphs.

Is it a tree?

  • No (for this specific traversal pattern): While it IS a tree structurally, the level-order traversal requirement doesn't follow the typical DFS pattern for trees. Level-order traversal is inherently about processing nodes by their distance from the root.

Is the problem related to directed acyclic graphs?

  • No: This is about level-order traversal, not DAG-specific problems.

Is the problem related to shortest paths?

  • No: We're not finding shortest paths, we're collecting all nodes by levels.

Does the problem involve connectivity?

  • No: We're not checking if nodes are connected; we're traversing all nodes level by level.

Does the problem have small constraints?

  • No: Tree size can be large (up to 2000 nodes typically in LeetCode).

BFS?

  • Yes: BFS is the natural choice for level-order traversal as it processes nodes level by level using a queue.

Conclusion: The flowchart leads us to BFS (Breadth-First Search) for this problem. BFS is perfect for level-order traversal because it naturally processes all nodes at distance k from the root before processing nodes at distance k+1, which is exactly what we need for collecting nodes level by level. The final reversal step (ans[::-1]) transforms the top-down level order into the required bottom-up format.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to traverse a binary tree level by level, BFS immediately comes to mind because of its natural property: it explores all nodes at the current distance from the source before moving to nodes at the next distance. This is exactly what level-order traversal requires.

Think of BFS like ripples in water - when you drop a stone, the ripples expand outward uniformly, reaching all points at the same distance before moving further out. Similarly, BFS uses a queue to process all nodes at level 0 (the root), then all nodes at level 1, then level 2, and so on.

The key insight for this problem is recognizing that we need two things:

  1. Level-by-level processing: We need to know which nodes belong to the same level
  2. Bottom-up ordering: We need the deepest level first

For the first requirement, we use a clever technique with the queue. At the start of processing each level, we record the current queue size - this tells us exactly how many nodes belong to the current level. We then process exactly that many nodes, collecting their values and adding their children to the queue for the next level.

For the second requirement, we have a simple observation: if we collect levels in the normal top-down order (root to leaves), we can simply reverse the entire result at the end to get bottom-up order. This is much simpler than trying to insert levels at the beginning of our result list (which would be inefficient) or calculating the tree height first.

The pattern for _ in range(len(q)) is the crucial part - it ensures we process exactly one level at a time, maintaining the level boundaries even as we're adding new nodes (children) to the queue. This way, we naturally separate nodes by their levels without needing any additional data structures or depth tracking.

Solution Approach

The solution implements BFS using a queue data structure (deque) to achieve level-order traversal. Here's how the algorithm works step by step:

Initial Setup:

  • Create an empty result list ans to store each level's values
  • Handle the edge case: if the root is None, return the empty list immediately
  • Initialize a queue with the root node: q = deque([root])

Level-by-Level Processing: The main algorithm uses a while loop that continues as long as there are nodes in the queue:

  1. Level Isolation: At the start of each iteration, we create a temporary list t to store the current level's values. The key insight is using len(q) to determine how many nodes belong to the current level - this is the number of nodes we need to process before moving to the next level.

  2. Process Current Level: We loop exactly len(q) times (the number saved at the start of the level):

    • Dequeue a node using q.popleft()
    • Add its value to the temporary list: t.append(node.val)
    • Enqueue its children (if they exist) for the next level:
      • if node.left: q.append(node.left)
      • if node.right: q.append(node.right)
  3. Store Level Results: After processing all nodes in the current level, append the temporary list t to our answer: ans.append(t)

Final Reversal: After collecting all levels in top-down order, we reverse the entire result using Python's slice notation: return ans[::-1]. This transforms the natural top-down BFS order into the required bottom-up order.

Why This Works:

  • The queue ensures we process nodes in FIFO order, maintaining left-to-right traversal within each level
  • By capturing len(q) at the start of each level, we know exactly when one level ends and the next begins, even as we're adding new nodes to the queue
  • The reversal at the end is an O(n) operation where n is the number of levels, which is much more efficient than inserting at the beginning of the list for each level

The time complexity is O(N) where N is the total number of nodes, as we visit each node exactly once. The space complexity is also O(N) for storing the result and the queue (which can hold at most one level of nodes at a time).

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with a simple binary tree:

      1
     / \
    2   3
   /
  4

Step 1: Initialization

  • ans = [] (will store our levels)
  • q = deque([1]) (queue starts with root node)

Step 2: Process Level 0 (Root Level)

  • Current queue: [1], queue length = 1
  • Create temporary list: t = []
  • Process 1 node (the saved queue length):
    • Dequeue node 1: t = [1]
    • Add children to queue: node 2 (left) and node 3 (right)
    • Queue becomes: [2, 3]
  • Append level to answer: ans = [[1]]

Step 3: Process Level 1

  • Current queue: [2, 3], queue length = 2
  • Create temporary list: t = []
  • Process 2 nodes:
    • Dequeue node 2: t = [2]
      • Add left child (node 4) to queue
      • Queue becomes: [3, 4]
    • Dequeue node 3: t = [2, 3]
      • No children to add
      • Queue remains: [4]
  • Append level to answer: ans = [[1], [2, 3]]

Step 4: Process Level 2

  • Current queue: [4], queue length = 1
  • Create temporary list: t = []
  • Process 1 node:
    • Dequeue node 4: t = [4]
    • No children to add
    • Queue becomes empty: []
  • Append level to answer: ans = [[1], [2, 3], [4]]

Step 5: Final Reversal

  • Queue is empty, exit while loop
  • Reverse the answer: ans[::-1] = [[4], [2, 3], [1]]

Final Result: [[4], [2, 3], [1]]

The key insight is how len(q) captures the exact number of nodes in each level. When processing level 1, even though we're adding node 4 to the queue while processing, we only process the 2 nodes that were originally in the queue for that level. This maintains clean level boundaries throughout the traversal.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from collections import deque
9from typing import List, Optional
10
11class Solution:
12    def levelOrderBottom(self, root: Optional[TreeNode]) -> List[List[int]]:
13        """
14        Performs bottom-up level order traversal of a binary tree.
15        Returns a list of lists where each inner list contains values of nodes at the same level,
16        ordered from bottom (leaves) to top (root).
17      
18        Args:
19            root: The root node of the binary tree
20          
21        Returns:
22            A list of lists containing node values in bottom-up level order
23        """
24        # Initialize result list to store level values
25        result = []
26      
27        # Handle empty tree case
28        if root is None:
29            return result
30      
31        # Initialize queue with root node for BFS traversal
32        queue = deque([root])
33      
34        # Process tree level by level using BFS
35        while queue:
36            # Store current level's node values
37            current_level = []
38          
39            # Process all nodes at the current level
40            level_size = len(queue)
41            for _ in range(level_size):
42                # Remove and process the front node
43                current_node = queue.popleft()
44                current_level.append(current_node.val)
45              
46                # Add child nodes to queue for next level processing
47                if current_node.left:
48                    queue.append(current_node.left)
49                if current_node.right:
50                    queue.append(current_node.right)
51          
52            # Add current level values to result
53            result.append(current_level)
54      
55        # Reverse the result to get bottom-up order
56        return result[::-1]
57
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    /**
18     * Returns the level order traversal of a binary tree from bottom to top.
19     * Each level is represented as a list of node values.
20     * 
21     * @param root The root node of the binary tree
22     * @return A list of lists containing node values at each level (bottom-up order)
23     */
24    public List<List<Integer>> levelOrderBottom(TreeNode root) {
25        // Use LinkedList to efficiently add elements at the beginning
26        LinkedList<List<Integer>> result = new LinkedList<>();
27      
28        // Handle empty tree case
29        if (root == null) {
30            return result;
31        }
32      
33        // Queue for BFS traversal
34        Deque<TreeNode> queue = new LinkedList<>();
35        queue.offerLast(root);
36      
37        // Process tree level by level
38        while (!queue.isEmpty()) {
39            // Store current level's node values
40            List<Integer> currentLevel = new ArrayList<>();
41          
42            // Get the number of nodes at current level
43            int levelSize = queue.size();
44          
45            // Process all nodes at current level
46            for (int i = 0; i < levelSize; i++) {
47                TreeNode currentNode = queue.pollFirst();
48                currentLevel.add(currentNode.val);
49              
50                // Add left child to queue for next level processing
51                if (currentNode.left != null) {
52                    queue.offerLast(currentNode.left);
53                }
54              
55                // Add right child to queue for next level processing
56                if (currentNode.right != null) {
57                    queue.offerLast(currentNode.right);
58                }
59            }
60          
61            // Add current level at the beginning to achieve bottom-up order
62            result.addFirst(currentLevel);
63        }
64      
65        return result;
66    }
67}
68
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    vector<vector<int>> levelOrderBottom(TreeNode* root) {
15        // Result vector to store level order traversal from bottom to top
16        vector<vector<int>> result;
17      
18        // Handle edge case: empty tree
19        if (!root) {
20            return result;
21        }
22      
23        // Initialize queue for BFS traversal with root node
24        queue<TreeNode*> nodeQueue;
25        nodeQueue.push(root);
26      
27        // Perform level-order traversal using BFS
28        while (!nodeQueue.empty()) {
29            // Vector to store current level's node values
30            vector<int> currentLevel;
31          
32            // Process all nodes at the current level
33            int levelSize = nodeQueue.size();
34            for (int i = 0; i < levelSize; ++i) {
35                // Get and remove the front node from queue
36                TreeNode* currentNode = nodeQueue.front();
37                nodeQueue.pop();
38              
39                // Add current node's value to current level
40                currentLevel.push_back(currentNode->val);
41              
42                // Add left child to queue if it exists
43                if (currentNode->left) {
44                    nodeQueue.push(currentNode->left);
45                }
46              
47                // Add right child to queue if it exists
48                if (currentNode->right) {
49                    nodeQueue.push(currentNode->right);
50                }
51            }
52          
53            // Add current level to result
54            result.push_back(currentLevel);
55        }
56      
57        // Reverse the result to get bottom-up order
58        reverse(result.begin(), result.end());
59      
60        return result;
61    }
62};
63
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Performs bottom-up level order traversal of a binary tree
17 * Returns the values of nodes level by level from bottom to top
18 * @param root - The root node of the binary tree
19 * @returns A 2D array where each sub-array contains node values at each level (bottom to top)
20 */
21function levelOrderBottom(root: TreeNode | null): number[][] {
22    // Initialize result array to store level-wise node values
23    const result: number[][] = [];
24  
25    // Handle empty tree case
26    if (!root) {
27        return result;
28    }
29  
30    // Initialize queue with root node for BFS traversal
31    const queue: TreeNode[] = [root];
32  
33    // Process nodes level by level
34    while (queue.length > 0) {
35        // Array to store current level's node values
36        const currentLevel: number[] = [];
37        // Temporary queue to store next level's nodes
38        const nextLevelQueue: TreeNode[] = [];
39      
40        // Process all nodes in the current level
41        for (const node of queue) {
42            // Destructure node properties
43            const { val, left, right } = node;
44          
45            // Add current node's value to current level array
46            currentLevel.push(val);
47          
48            // Add left child to next level queue if it exists
49            if (left) {
50                nextLevelQueue.push(left);
51            }
52          
53            // Add right child to next level queue if it exists
54            if (right) {
55                nextLevelQueue.push(right);
56            }
57        }
58      
59        // Add current level values to result
60        result.push(currentLevel);
61      
62        // Replace queue contents with next level nodes
63        queue.splice(0, queue.length, ...nextLevelQueue);
64    }
65  
66    // Reverse the result to get bottom-up order
67    return result.reverse();
68}
69

Time and Space Complexity

Time Complexity: O(n)

The algorithm uses BFS (Breadth-First Search) to traverse the binary tree level by level. Each node in the tree is visited exactly once - it's added to the queue once and processed once when it's dequeued. The operations performed for each node (appending to list, checking children, adding children to queue) are all O(1) operations. Therefore, with n nodes in the tree, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity comes from multiple sources:

  • The queue q stores nodes at each level. In the worst case (a complete binary tree), the maximum number of nodes at any level is approximately n/2 at the last level, giving O(n) space.
  • The result list ans stores all n node values organized by levels, which requires O(n) space.
  • The temporary list t for each level contributes to space usage, but since it's included in the final result, it doesn't add extra complexity.
  • The list reversal operation ans[::-1] creates a new list with n elements, which is also O(n) space.

Overall, the space complexity is O(n) where n is the number of nodes in the binary tree.

Common Pitfalls

Pitfall 1: Incorrectly Capturing Level Size

The Problem: A common mistake is not properly capturing the level size before processing nodes. Consider this incorrect implementation:

while queue:
    current_level = []
    # WRONG: Using queue length dynamically
    for i in range(len(queue)):  # This changes as we add children!
        node = queue.popleft()
        current_level.append(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)

Or even worse:

while queue:
    current_level = []
    # WRONG: Processing all nodes in queue without level distinction
    while queue:  # This processes ALL remaining nodes, not just current level
        node = queue.popleft()
        current_level.append(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
    result.append(current_level)

Why It Fails: In the first case, if we don't save len(queue) to a variable first, the loop might process more nodes than intended because we're adding children during iteration. In the second case, we'd end up with all remaining nodes in a single level instead of maintaining proper level separation.

The Solution: Always capture the queue size at the beginning of each level iteration:

level_size = len(queue)  # Capture BEFORE the loop
for _ in range(level_size):  # Process exactly this many nodes
    # ... process nodes

Pitfall 2: Using List Insert Instead of Reverse

The Problem: Some might try to avoid the final reversal by inserting at the beginning:

while queue:
    current_level = []
    level_size = len(queue)
    for _ in range(level_size):
        node = queue.popleft()
        current_level.append(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
  
    # INEFFICIENT: Inserting at index 0
    result.insert(0, current_level)  # O(n) operation each time!

Why It's Bad: Using insert(0, element) has O(n) time complexity for each insertion because it needs to shift all existing elements. If the tree has k levels, this approach has O(k²) complexity for the insertions alone.

The Solution: Stick with appending (O(1) operation) and reverse once at the end:

result.append(current_level)  # O(1) for each level
# ... after all levels processed
return result[::-1]  # Single O(k) reversal where k = number of levels

Pitfall 3: Not Handling None Children Properly

The Problem: Forgetting to check if children exist before adding them to the queue:

# WRONG: May add None values to queue
queue.append(node.left)   # What if node.left is None?
queue.append(node.right)  # What if node.right is None?

Why It Fails: This would add None values to the queue, causing an AttributeError when trying to access .val or .left/.right on a None object in the next iteration.

The Solution: Always check for None before adding to queue:

if node.left:
    queue.append(node.left)
if node.right:
    queue.append(node.right)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More