LeetCode 107 - Binary Tree Level Order Traversal II
The problem asks us to traverse a binary tree and return the node values level by level, but in bottom-up order. Normally, a level order traversal collects values from the root down to the leaves.
Difficulty: 🟡 Medium
Topics: Tree, Breadth-First Search, Binary Tree
Solution
Problem Understanding
The problem asks us to traverse a binary tree and return the node values level by level, but in bottom-up order. Normally, a level order traversal collects values from the root down to the leaves. Here, we need to reverse that order and return the leaf levels first, moving up to the root.
The input is a standard binary tree represented by its root node. Each node contains an integer value and pointers to its left and right children. The output is a list of lists of integers, where each inner list corresponds to a level in the tree, starting from the bottom-most level and moving up to the root.
The constraints specify that the tree can have up to 2000 nodes, and node values range from -1000 to 1000. This suggests that the algorithm should ideally run in linear time relative to the number of nodes. Edge cases include an empty tree (root = None) and trees with only one node.
Approaches
A brute-force approach would perform a standard level order traversal (BFS), storing each level in a list as we go from top to bottom. Once all levels are collected, we could reverse the list of levels at the end. While this works, it uses extra space for the level storage and an additional reverse operation. It is correct but slightly inefficient in terms of space and post-processing.
The key insight for an optimal solution is that we can still perform BFS but prepend each level to the result list as we traverse. This way, the bottom levels naturally end up at the start of the result list, eliminating the need for a separate reverse operation. BFS ensures that we visit nodes level by level, and prepending preserves the bottom-up order efficiently.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Perform BFS top-down and reverse the list at the end |
| Optimal | O(n) | O(n) | Perform BFS and prepend each level to the result list |
Algorithm Walkthrough
- Handle edge case: If
rootisNone, return an empty list immediately. This prevents unnecessary processing for empty trees. - Initialize BFS structures: Use a queue to facilitate BFS. Start by enqueuing the root node. Create an empty result list to store levels in bottom-up order.
- Process each level: While the queue is not empty, determine the number of nodes in the current level (
level_size). Create a temporary list to store values for this level. - Traverse nodes at the current level: For each node in the level, dequeue it, add its value to the temporary list, and enqueue its non-null left and right children.
- Prepend level: Once all nodes of the current level are processed, insert the temporary list at the start of the result list. This ensures bottom-up ordering without reversing at the end.
- Return result: After the queue is empty, all levels are processed, and the result list contains levels from bottom to top.
Why it works: BFS guarantees that nodes are visited level by level from top to bottom. By prepending each level to the result, the last levels (leaves) naturally appear first, satisfying the bottom-up requirement. Each node is visited exactly once, ensuring correctness and efficiency.
Python Solution
from typing import List, Optional
from collections import deque
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def levelOrderBottom(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.insert(0, level) # Prepend the current level
return result
The Python implementation uses deque for efficient queue operations. Each level's nodes are collected in a temporary list, then inserted at the start of the result list to maintain bottom-up order. Edge cases like empty trees are handled upfront.
Go Solution
package main
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func levelOrderBottom(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
result := [][]int{}
queue := []*TreeNode{root}
for len(queue) > 0 {
levelSize := len(queue)
level := []int{}
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
level = append(level, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
// Prepend level
result = append([][]int{level}, result...)
}
return result
}
In Go, slices are used to simulate a queue, and prepending a slice is done by concatenation. nil checks replace Python's None checks, and slices dynamically grow to store each level.
Worked Examples
Example 1: root = [3,9,20,null,null,15,7]
| Step | Queue | Level | Result |
|---|---|---|---|
| 1 | [3] | [] | [] |
| 2 | [9,20] | [3] | [[3]] → prepended → [[3]] |
| 3 | [15,7] | [9,20] | [[9,20],[3]] |
| 4 | [] | [15,7] | [[15,7],[9,20],[3]] |
Example 2: root = [1]
| Step | Queue | Level | Result |
|---|---|---|---|
| 1 | [1] | [1] | [[1]] |
Example 3: root = []
Queue is empty from the start, return [].
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each node is visited exactly once in BFS |
| Space | O(n) | Queue may store up to n/2 nodes in the last level, plus result list stores all nodes |
The complexity is linear because every node is processed once, and all levels are stored in memory.
Test Cases
# Basic example trees
assert Solution().levelOrderBottom(None) == [] # empty tree
assert Solution().levelOrderBottom(TreeNode(1)) == [[1]] # single node
assert Solution().levelOrderBottom(TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7)))) == [[15,7],[9,20],[3]] # balanced tree
# Left-heavy tree
assert Solution().levelOrderBottom(TreeNode(1, TreeNode(2, TreeNode(3)))) == [[3],[2],[1]]
# Right-heavy tree
assert Solution().levelOrderBottom(TreeNode(1, None, TreeNode(2, None, TreeNode(3)))) == [[3],[2],[1]]
# Mixed tree
assert Solution().levelOrderBottom(TreeNode(1, TreeNode(2, TreeNode(4)), TreeNode(3))) == [[4],[2,3],[1]]
| Test | Why |
|---|---|
None |
Edge case for empty tree |
| Single node | Simplest non-empty tree |
| Balanced tree | Standard case |
| Left-heavy | Tests handling missing right children |
| Right-heavy | Tests handling missing left children |
| Mixed | Checks correctness for irregular trees |
Edge Cases
Empty tree: If the root is None, the queue never gets initialized with nodes. The algorithm immediately returns an empty list, preventing null reference errors.
Single node tree: Only one node exists. The BFS loop runs once, adding a list with a single element and prepending it. The result is correct, demonstrating the algorithm handles minimal input.
Highly unbalanced trees: Trees skewed to the left or right can break naive indexing approaches. Our BFS approach uses a queue, which naturally handles missing children by skipping None/nil nodes, ensuring correct bottom-up ordering without extra checks.