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:
- Start with the root node in a queue
- For each level, process all nodes currently in the queue
- Collect their values and add their children to the queue for the next level
- Continue until all levels are processed
- 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.
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:
- Level-by-level processing: We need to know which nodes belong to the same level
- 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:
-
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 usinglen(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. -
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)
- Dequeue a node using
-
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 EvaluatorExample 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]
- Dequeue node 1:
- 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]
- Dequeue node 2:
- 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:
[]
- Dequeue node 4:
- 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 approximatelyn/2
at the last level, givingO(n)
space. - The result list
ans
stores alln
node values organized by levels, which requiresO(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 withn
elements, which is alsoO(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)
Which of the following is a good use case for backtracking?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!