140. Binary Tree Level Order Traversal
🔗 LeetCode Problem: 102. Binary Tree Level Order Traversal
📊 Difficulty: Medium
🏷️ Topics: Binary Trees, BFS, Queue, Level Order, DFS
Problem Statement
Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level).
Example 1:
Input: root = [3,9,20,null,null,15,7]
Output: [[3],[9,20],[15,7]]
Example 2:
Input: root = [1]
Output: [[1]]
Example 3:
Input: root = []
Output: []
Constraints:
- The number of nodes in the tree is in the range [0, 2000]
- -1000 <= Node.val <= 1000
🌳 Visual Understanding - Level by Level!
What is Level Order Traversal?
Level Order = Visit nodes LEVEL BY LEVEL
Also called: Breadth-First Search (BFS)
Tree:
3 ← Level 0
/ \
9 20 ← Level 1
/ \
15 7 ← Level 2
Level 0: [3]
Level 1: [9, 20]
Level 2: [15, 7]
Result: [[3], [9, 20], [15, 7]]
Pattern: Horizontal layers, left to right!
Example 1: Complete Visual
Tree:
3 ← Level 0: [3]
/ \
9 20 ← Level 1: [9, 20]
/ \
15 7 ← Level 2: [15, 7]
Processing order:
Step 1: Visit level 0 → [3]
Step 2: Visit level 1 → [9, 20]
Step 3: Visit level 2 → [15, 7]
Output: [[3], [9, 20], [15, 7]]
Each level is a separate list!
Example 2: Unbalanced Tree
Tree:
1
/ \
2 3
/ \ \
4 5 6
/
7
Level 0: [1]
Level 1: [2, 3]
Level 2: [4, 5, 6]
Level 3: [7]
Output: [[1], [2, 3], [4, 5, 6], [7]]
Levels can have different sizes!
🧠 The AHA Moment - BFS vs DFS!
DFS (What We've Been Using):
DFS = Depth-First Search
Goes DEEP before WIDE
Inorder example:
1
/ \
2 3
/ \
4 5
Order: 4 → 2 → 5 → 1 → 3
Pattern: Goes deep (to 4) before exploring siblings
Visual path: 1 → 2 → 4 (deep!) → back → 5 → back → back → 3
BFS (Level Order):
BFS = Breadth-First Search
Goes WIDE before DEEP
Level order:
1
/ \
2 3
/ \
4 5
Order: 1 → 2 → 3 → 4 → 5
Pattern: Complete each level before going deeper
Visual path: Level 0 (1) → Level 1 (2,3) → Level 2 (4,5)
Key Difference:
DFS uses: STACK (or recursion call stack)
- Last In, First Out (LIFO)
- Goes deep like a stack of plates
BFS uses: QUEUE
- First In, First Out (FIFO)
- Goes wide like a waiting line
This is THE fundamental difference!
🧠 BFS Strategy - Using Queue
How Queue Works for BFS:
Queue = FIFO (First In, First Out)
Process:
1. Add root to queue
2. While queue not empty:
a. Count how many nodes at current level
b. Process all nodes at current level
c. Add their children (next level) to queue
3. Repeat for each level
Why this works:
- Queue naturally processes level by level
- All nodes of level N come before level N+1
- Perfect for BFS!
Visual Queue Operation:
Tree:
1
/ \
2 3
Initial:
queue = [1]
result = []
Process Level 0:
levelSize = 1 (queue has 1 node)
Process node 1:
Remove 1 from queue
Add 1 to currentLevel → [1]
Add children: 2, 3 to queue
queue = [2, 3]
result = [[1]]
Process Level 1:
levelSize = 2 (queue has 2 nodes)
Process node 2:
Remove 2 from queue
Add 2 to currentLevel → [2]
Add children: none
Process node 3:
Remove 3 from queue
Add 3 to currentLevel → [2, 3]
Add children: none
queue = []
result = [[1], [2, 3]]
Done! Queue is empty.
🎯 Solution 1: BFS with Queue (Standard Approach)
Implementation:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
// Edge case: empty tree
if (root == null) {
return result;
}
// Create queue and add root
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
// Process level by level
while (!queue.isEmpty()) {
// Get number of nodes at current level
// WHY? Need to process exactly this many nodes
int levelSize = queue.size();
// List for current level
List<Integer> currentLevel = new ArrayList<>();
// Process all nodes at current level
for (int i = 0; i < levelSize; i++) {
// Remove node from queue
TreeNode node = queue.poll();
// Add to current level
currentLevel.add(node.val);
// Add children to queue (for next level)
// WHY? They'll be processed in next iteration
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
// Add current level to result
result.add(currentLevel);
}
return result;
}
}
The Critical Pattern:
while (!queue.isEmpty()) {
int levelSize = queue.size(); // ← CRITICAL!
for (int i = 0; i < levelSize; i++) {
// Process exactly levelSize nodes
// This ensures we process one complete level
}
}
Without levelSize:
- Would process nodes as they're added
- Can't separate levels
- Results would be mixed!
With levelSize:
- Process exactly one level per iteration
- Clear separation between levels
- Perfect level-by-level processing!
🎯 Solution 2: DFS with Level Tracking (Less Common)
Can We Use DFS?
YES! But it's less intuitive.
Key idea:
Track the LEVEL (depth) as we recurse
Add node to the list at that level index
Example:
recurse(node, level):
- Add node to result[level]
- Recurse on left with level+1
- Recurse on right with level+1
Implementation:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
dfs(root, 0, result);
return result;
}
private void dfs(TreeNode node, int level, List<List<Integer>> result) {
// Base case: null node
if (node == null) {
return;
}
// If this is first node at this level, create new list
// WHY? Need a list for each level
if (level == result.size()) {
result.add(new ArrayList<>());
}
// Add current node to its level
result.get(level).add(node.val);
// Recurse on children with increased level
dfs(node.left, level + 1, result);
dfs(node.right, level + 1, result);
}
}
Why DFS Works But Is Less Natural:
DFS order: 1 → 2 → 4 → 5 → 3 → 6
But we track levels:
1 at level 0 → result[0] = [1]
2 at level 1 → result[1] = [2]
4 at level 2 → result[2] = [4]
5 at level 2 → result[2] = [4, 5]
3 at level 1 → result[1] = [2, 3]
6 at level 2 → result[2] = [4, 5, 6]
Final: [[1], [2, 3], [4, 5, 6]]
Works! But order of adding is not level-by-level.
BFS is more natural for this problem.
🔍 Detailed Dry Run - BFS Approach
Tree:
3
/ \
9 20
/ \
15 7
Complete Queue Operations:
INITIAL STATE:
queue = [3]
result = []
═══════════════════════════════════════
ITERATION 1 - Process Level 0:
═══════════════════════════════════════
levelSize = queue.size() = 1
currentLevel = []
For loop (i = 0 to 0):
i = 0:
node = queue.poll() = 3
currentLevel.add(3) → [3]
3.left = 9, add to queue
3.right = 20, add to queue
queue = [9, 20]
Add currentLevel to result → [[3]]
State:
queue = [9, 20]
result = [[3]]
═══════════════════════════════════════
ITERATION 2 - Process Level 1:
═══════════════════════════════════════
levelSize = queue.size() = 2 ← Two nodes!
currentLevel = []
For loop (i = 0 to 1):
i = 0:
node = queue.poll() = 9
currentLevel.add(9) → [9]
9.left = null, skip
9.right = null, skip
queue = [20]
i = 1:
node = queue.poll() = 20
currentLevel.add(20) → [9, 20]
20.left = 15, add to queue
20.right = 7, add to queue
queue = [15, 7]
Add currentLevel to result → [[3], [9, 20]]
State:
queue = [15, 7]
result = [[3], [9, 20]]
═══════════════════════════════════════
ITERATION 3 - Process Level 2:
═══════════════════════════════════════
levelSize = queue.size() = 2
currentLevel = []
For loop (i = 0 to 1):
i = 0:
node = queue.poll() = 15
currentLevel.add(15) → [15]
15.left = null, skip
15.right = null, skip
queue = [7]
i = 1:
node = queue.poll() = 7
currentLevel.add(7) → [15, 7]
7.left = null, skip
7.right = null, skip
queue = []
Add currentLevel to result → [[3], [9, 20], [15, 7]]
State:
queue = []
result = [[3], [9, 20], [15, 7]]
═══════════════════════════════════════
DONE! Queue is empty.
═══════════════════════════════════════
FINAL RESULT: [[3], [9, 20], [15, 7]]
🔍 Detailed Dry Run - DFS Approach
Tree:
1
/ \
2 3
Complete DFS Recursion:
CALL STACK VISUALIZATION:
Level 0: dfs(1, 0, [])
│
├─ node = 1, level = 0, result = []
├─ level == result.size()? 0 == 0 → YES
├─ Create new list: result = [[]]
├─ Add 1 to result[0]: result = [[1]]
│
├─ LEFT: dfs(2, 1, [[1]]) ──────────────────┐
│ │
│ Level 1: dfs(2, 1, [[1]]) │
│ │ │
│ ├─ node = 2, level = 1 │
│ ├─ level == result.size()? 1 == 1 → YES │
│ ├─ Create new list: result = [[1], []] │
│ ├─ Add 2: result = [[1], [2]] │
│ │ │
│ ├─ LEFT: dfs(null, 2, [[1], [2]]) │
│ │ └─ return (null) │
│ │ │
│ ├─ RIGHT: dfs(null, 2, [[1], [2]]) │
│ │ └─ return (null) │
│ │ │
│ └─ return │
│ │
├─ After left child processed │
├─ result = [[1], [2]] │
│ │
├─ RIGHT: dfs(3, 1, [[1], [2]]) ───────────┐
│ │
│ Level 1: dfs(3, 1, [[1], [2]]) │
│ │ │
│ ├─ node = 3, level = 1 │
│ ├─ level == result.size()? 1 == 2 → NO │
│ ├─ List exists! Just add │
│ ├─ Add 3: result = [[1], [2, 3]] │
│ │ │
│ ├─ LEFT: dfs(null, 2, [[1], [2, 3]]) │
│ │ └─ return (null) │
│ │ │
│ ├─ RIGHT: dfs(null, 2, [[1], [2, 3]]) │
│ │ └─ return (null) │
│ │ │
│ └─ return │
│ │
└─ FINAL: result = [[1], [2, 3]] │
Result: [[1], [2, 3]]
📊 BFS vs DFS Comparison
BFS (Queue-based):
✓ Natural for level order
✓ Processes levels in order
✓ Iterative (no recursion)
✓ Easy to understand for this problem
✗ Requires queue data structure
Code simplicity: ⭐⭐⭐⭐⭐
Intuitiveness: ⭐⭐⭐⭐⭐
Use when: Level-by-level processing needed
DFS (Recursion with level tracking):
✓ Uses recursion (elegant)
✓ No extra data structure needed
✗ Less intuitive for level order
✗ Adds nodes out of level order (but still correct!)
Code simplicity: ⭐⭐⭐
Intuitiveness: ⭐⭐
Use when: Want to avoid queue, prefer recursion
When to Use Each:
For Level Order problems:
→ Use BFS (Queue)
→ It's the natural choice!
For Depth problems:
→ Use DFS (Recursion)
→ Natural for going deep
For other tree problems:
→ Either works
→ Choose based on problem needs
📊 Complexity Analysis
BFS Solution:
Time Complexity: O(n)
Where n = number of nodes
Visit each node exactly once
At each node: O(1) operations
Total: n × O(1) = O(n)
All nodes must be visited!
Space Complexity: O(n)
Queue: O(w) where w = maximum width
Result: O(n) for storing all nodes
Width calculation:
- Worst case: Complete tree last level
- Last level has n/2 nodes
- So w = O(n)
Total: O(n) + O(n) = O(n)
DFS Solution:
Time Complexity: O(n)
Same as BFS - visit each node once
Space Complexity: O(n)
Recursion stack: O(h) where h = height
Result: O(n)
Best case (balanced): O(log n) + O(n) = O(n)
Worst case (skewed): O(n) + O(n) = O(n)
Total: O(n)
⚠️ Common Mistakes
Mistake 1: Not Using levelSize
// ❌ WRONG - Can't separate levels!
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
result.add(node.val); // Where does this level end?
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
// Result: [3, 9, 20, 15, 7] - flat list, not levels!
// ✓ CORRECT - Use levelSize
while (!queue.isEmpty()) {
int levelSize = queue.size(); // Critical!
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
// ...
}
result.add(currentLevel);
}
Mistake 2: Wrong Loop Condition
// ❌ WRONG - Using queue.size() in loop condition
while (!queue.isEmpty()) {
// Size changes as we add children!
for (int i = 0; i < queue.size(); i++) {
TreeNode node = queue.poll();
// ...
queue.offer(node.left); // Size increased!
queue.offer(node.right); // Size increased again!
}
}
// Loop runs wrong number of times!
// ✓ CORRECT - Capture size before loop
int levelSize = queue.size(); // Fixed value!
for (int i = 0; i < levelSize; i++) {
// Process exactly levelSize nodes
}
Mistake 3: Not Creating New List for Each Level
// ❌ WRONG - Reusing same list
List<Integer> currentLevel = new ArrayList<>();
while (!queue.isEmpty()) {
int levelSize = queue.size();
for (int i = 0; i < levelSize; i++) {
currentLevel.add(node.val);
}
result.add(currentLevel);
currentLevel.clear(); // Clears in result too!
}
// All levels reference same list!
// After clear(), all are empty!
// ✓ CORRECT - New list for each level
while (!queue.isEmpty()) {
List<Integer> currentLevel = new ArrayList<>(); // New!
// ...
result.add(currentLevel);
}
Mistake 4: DFS - Wrong Level Check
// ❌ WRONG - Creating lists incorrectly
private void dfs(TreeNode node, int level, List<List<Integer>> result) {
if (node == null) return;
if (result.size() < level) { // Wrong comparison!
result.add(new ArrayList<>());
}
result.get(level).add(node.val);
// ...
}
// Creates too many or too few lists!
// ✓ CORRECT - Check if level == size
if (level == result.size()) { // Exactly right!
result.add(new ArrayList<>());
}
Mistake 5: Forgetting Null Check for Children
// ❌ WRONG - Adding null to queue
TreeNode node = queue.poll();
queue.offer(node.left); // Might be null!
queue.offer(node.right); // Might be null!
// Queue fills with nulls!
// NullPointerException when processing!
// ✓ CORRECT - Check before adding
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
🎯 Pattern Recognition - Level-Based Problems
Core Pattern: Level Order Processing
Template for level-based problems:
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
// Process current level
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
// Do something with node
process(node);
// Add children for next level
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
// After level processing
doSomethingAfterLevel();
}
Related Problems:
1. Binary Tree Zigzag Level Order
Same as level order but alternate direction
Level 0: left to right
Level 1: right to left
Level 2: left to right
Use same BFS + reverse alternate levels
2. Binary Tree Right Side View
Return rightmost node of each level
Use level order traversal
Only return last node of each level
3. Average of Levels
Calculate average value per level
Use level order traversal
Calculate sum / count for each level
4. Minimum Depth (Problem 134)
BFS is BETTER than DFS!
First leaf found = minimum depth
Early return optimization
5. Maximum Width of Binary Tree
Find maximum width of any level
Use level order with position tracking
Calculate max width per level
When to Use This Pattern:
✓ "Level" in problem statement
✓ "Layer" or "row" mentioned
✓ "Left to right at each level"
✓ Need to process tree horizontally
✓ Level-based calculations
📝 Quick Revision Notes
🎯 Core Concept
Level Order = BFS = Visit tree level by level. Use Queue (FIFO). Critical pattern: capture levelSize = queue.size() BEFORE loop, process exactly that many nodes. This separates levels. Add children to queue for next level. Alternative: DFS with level tracking (less natural). Queue is standard for level-based problems.
⚡ Quick Implementation
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
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 static TreeNode buildTree(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int i = 1;
while (!queue.isEmpty() && i < arr.length) {
TreeNode current = queue.poll();
// Process left child
if (i < arr.length) {
if (arr[i] != null) {
current.left = new TreeNode(arr[i]);
queue.offer(current.left);
}
i++;
}
// Process right child
if (i < arr.length) {
if (arr[i] != null) {
current.right = new TreeNode(arr[i]);
queue.offer(current.right);
}
i++;
}
}
return root;
}
public static void print(TreeNode root) {
if (root == null) {
System.out.println("[]");
return;
}
List<String> result = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node != null) {
result.add(String.valueOf(node.val));
queue.offer(node.left);
queue.offer(node.right);
} else {
result.add("null");
}
}
while (!result.isEmpty() && result.get(result.size() - 1).equals("null")) {
result.remove(result.size() - 1);
}
System.out.println(result);
}
}
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) {
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> temp = new ArrayList<>();
// process the current level.
int size = queue.size();
for(int i = 0; i < size; i++) {
TreeNode curr = queue.poll();
temp.add(curr.val);
if(curr.left != null) {
queue.offer(curr.left);
}
if(curr.right != null) {
queue.offer(curr.right);
}
}
res.add(new ArrayList<>(temp));
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.levelOrder(TreeNode.buildTree(new Integer[] {3,9,20,null,null,15,7})));
System.out.println(s.levelOrder(TreeNode.buildTree(new Integer[] {1})));
System.out.println(s.levelOrder(TreeNode.buildTree(new Integer[] {})));
}
}
private void dfs(TreeNode node, int level, List<List<Integer>> result) {
if (node == null) return;
if (level == result.size()) {
result.add(new ArrayList<>());
}
result.get(level).add(node.val);
dfs(node.left, level + 1, result);
dfs(node.right, level + 1, result);
}
🔑 Key Insights
BFS vs DFS:
BFS (Breadth-First):
- Uses Queue (FIFO)
- Goes WIDE (level by level)
- Natural for level order
- Example: 1 → 2 → 3 → 4 → 5
DFS (Depth-First):
- Uses Stack (or recursion)
- Goes DEEP (to leaves first)
- Natural for inorder/preorder/postorder
- Example: 1 → 2 → 4 → 5 → 3
Different tools for different jobs! 🔧
The levelSize Pattern:
Why capture size before loop?
while (!queue.isEmpty()) {
int levelSize = queue.size(); ← Snapshot!
for (i = 0 to levelSize) {
// Add children to queue
// Queue grows, but loop uses fixed levelSize
}
}
Without snapshot:
- Queue size changes during loop
- Can't separate levels
- Processes wrong number of nodes
This is the KEY to level-order! 🔑
Queue = Level Order:
Queue properties:
- FIFO (First In, First Out)
- Like a line at a store
- First person to arrive → First served
Perfect for BFS:
- Process parent before children
- All level N before level N+1
- Natural ordering!
Stack (DFS) = LIFO → Goes deep
Queue (BFS) = FIFO → Goes wide
🎪 Memory Aid
"Queue for levels, Stack for depths!"
"Capture size before the loop!"
"BFS goes wide, DFS goes deep!"
"FIFO for layers, LIFO for divers!" ✨
⚠️ Don't Forget
- Capture levelSize first (before loop modifies queue!)
- New list per level (don't reuse same list!)
- Check null before offering (don't add null to queue!)
- Use Queue not Stack (BFS needs FIFO!)
- Process entire level (exactly levelSize iterations!)
- BFS is natural (don't force DFS for level problems!)
🎉 Congratulations!
You've mastered BFS and level-order traversal!
What You Learned:
✅ BFS concept - Breadth-first search
✅ Queue usage - FIFO for level processing
✅ Level separation - The levelSize pattern
✅ BFS vs DFS - When to use each
✅ Two approaches - Queue-based and DFS-based
✅ Level-order template - Reusable pattern
The Fundamental Algorithms:
You now know BOTH traversal types:
DFS (Problem 139):
- Inorder, Preorder, Postorder
- Uses Stack or recursion
- Goes DEEP
BFS (Problem 140):
- Level Order
- Uses Queue
- Goes WIDE
These are THE TWO ways to traverse trees! 🌳
Every tree algorithm uses one of these!
Pattern Evolution:
Problems 131-138:
✓ Used DFS implicitly
✓ Recursion-based
✓ Depth-first thinking
Problem 139:
✓ Explicit DFS patterns
✓ Three traversal orders
✓ Recursive and iterative
Problem 140:
✓ BFS introduction!
✓ Queue-based traversal
✓ Level-by-level thinking
Foundation complete! 🏗️
Next Steps:
Perfect problems to apply BFS: - Zigzag Level Order (same pattern!) - Right Side View (last of each level!) - Average of Levels (level calculations!) - Minimum Depth (BFS early return!)
You now have BOTH traversal tools! 🧰🌳✨