223 lines
9.1 KiB
YAML
223 lines
9.1 KiB
YAML
title: Binary Tree Level Order Traversal II
|
||
slug: binary-tree-level-order-traversal-ii
|
||
difficulty: medium
|
||
leetcode_id: 107
|
||
leetcode_url: https://leetcode.com/problems/binary-tree-level-order-traversal-ii/
|
||
categories:
|
||
- trees
|
||
- queue
|
||
patterns:
|
||
- slug: bfs
|
||
is_optimal: true
|
||
- slug: tree-traversal
|
||
is_optimal: false
|
||
|
||
function_signature: "def level_order_bottom(root: TreeNode) -> list[list[int]]:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { root: [3, 9, 20, null, null, 15, 7] }
|
||
expected: [[15, 7], [9, 20], [3]]
|
||
- input: { root: [1] }
|
||
expected: [[1]]
|
||
- input: { root: [] }
|
||
expected: []
|
||
hidden:
|
||
- input: { root: [1, 2, 3, 4, 5, 6, 7] }
|
||
expected: [[4, 5, 6, 7], [2, 3], [1]]
|
||
- input: { root: [1, 2, null, 3, null, 4] }
|
||
expected: [[4], [3], [2], [1]]
|
||
- input: { root: [1, null, 2, null, 3] }
|
||
expected: [[3], [2], [1]]
|
||
- input: { root: [-10, 9, 20, null, null, 15, 7] }
|
||
expected: [[15, 7], [9, 20], [-10]]
|
||
|
||
description: |
|
||
Given the `root` of a binary tree, return *the bottom-up level order traversal of its nodes' values* (i.e., from left to right, level by level from leaf to root).
|
||
|
||
constraints: |
|
||
- The number of nodes in the tree is in the range `[0, 2000]`
|
||
- `-1000 <= Node.val <= 1000`
|
||
|
||
examples:
|
||
- input: "root = [3,9,20,null,null,15,7]"
|
||
output: "[[15,7],[9,20],[3]]"
|
||
explanation: "The deepest level (15, 7) comes first, then level 1 (9, 20), and finally the root level (3)."
|
||
- input: "root = [1]"
|
||
output: "[[1]]"
|
||
explanation: "Single node forms its own level."
|
||
- input: "root = []"
|
||
output: "[]"
|
||
explanation: "Empty tree returns an empty list."
|
||
|
||
explanation:
|
||
intuition: |
|
||
This problem is a twist on the classic level order traversal. Instead of reading the tree from top to bottom, we want to read it from bottom to top — like viewing a family photo starting with the grandchildren and working up to the grandparents.
|
||
|
||
Think of it like this: if we already know how to do a standard level order traversal (BFS), we get levels in the order `[root, level1, level2, ...]`. To get the bottom-up order, we simply **reverse the result** at the end: `[level2, level1, root]`.
|
||
|
||
The key insight is that **we don't need a fundamentally different algorithm**. BFS naturally processes the tree level by level from top to bottom. Rather than trying to traverse bottom-up (which is awkward), we traverse top-down and then reverse. This is both simpler and equally efficient.
|
||
|
||
Alternatively, we can build the result in reverse order by inserting each level at the front of the result list, but this is less efficient in most languages.
|
||
|
||
approach: |
|
||
We solve this using **BFS with Final Reversal**:
|
||
|
||
**Step 1: Handle the empty tree case**
|
||
|
||
- If `root` is `None`, return an empty list immediately
|
||
|
||
|
||
|
||
**Step 2: Initialise the queue and result**
|
||
|
||
- Create a queue starting with just the root node
|
||
- Create an empty result list to hold each level's values
|
||
|
||
|
||
|
||
**Step 3: Process level by level (standard BFS)**
|
||
|
||
- While the queue is not empty:
|
||
- Capture `level_size = len(queue)` — this is how many nodes are in the current level
|
||
- Create a `level_values` list for this level
|
||
- Process exactly `level_size` nodes:
|
||
- Dequeue a node, add its value to `level_values`
|
||
- Enqueue its left child (if exists)
|
||
- Enqueue its right child (if exists)
|
||
- Append `level_values` to the result
|
||
|
||
|
||
|
||
**Step 4: Reverse and return**
|
||
|
||
- Reverse the result list so the deepest level comes first
|
||
- Return the reversed result
|
||
|
||
|
||
|
||
The reversal is O(n) and doesn't change the overall complexity. This approach is clean because it reuses the well-known BFS level order pattern.
|
||
|
||
common_pitfalls:
|
||
- title: Over-Engineering the Traversal
|
||
description: |
|
||
A common mistake is trying to traverse the tree from bottom to top directly. This is unnecessarily complex — you'd need to first find the depth of the tree, then process nodes by depth in reverse order.
|
||
|
||
The simpler approach: do standard BFS (which is well-understood and easy to implement), then reverse the result at the end.
|
||
wrong_approach: "Trying to traverse bottom-up directly"
|
||
correct_approach: "Standard BFS, then reverse the result"
|
||
|
||
- title: Inserting at Front Instead of Reversing
|
||
description: |
|
||
Some solutions insert each level at index 0 of the result list: `result.insert(0, level_values)`. While this produces the correct output, `insert(0, ...)` on a Python list is O(n) for each insertion.
|
||
|
||
With k levels, this leads to O(n × k) operations. Appending and reversing at the end is O(n) total.
|
||
wrong_approach: "result.insert(0, level_values) for each level"
|
||
correct_approach: "result.append(level_values), then result.reverse() at the end"
|
||
|
||
- title: Using List as Queue
|
||
description: |
|
||
In Python, `list.pop(0)` is O(n) because all remaining elements must shift. For a tree with n nodes, this makes the algorithm O(n²) instead of O(n).
|
||
|
||
Use `collections.deque` which has O(1) `popleft()`.
|
||
wrong_approach: "queue = []; queue.pop(0)"
|
||
correct_approach: "queue = deque(); queue.popleft()"
|
||
|
||
- title: Forgetting the Empty Tree Check
|
||
description: |
|
||
If `root` is `None`, the queue starts empty, and the while loop never executes — but some implementations might crash trying to access root.
|
||
|
||
Always check `if not root: return []` at the start.
|
||
wrong_approach: "Assuming root is never None"
|
||
correct_approach: "if not root: return []"
|
||
|
||
key_takeaways:
|
||
- "**Transform, don't reinvent**: When a problem is a variation of a classic pattern, solve the classic version and transform the output"
|
||
- "**BFS = level order**: Breadth-first search naturally processes trees level by level — master this fundamental pattern"
|
||
- "**Reversal is cheap**: Reversing a list is O(n), same as the traversal — don't avoid it to pursue a 'clever' but slower solution"
|
||
- "**Same pattern, many problems**: This technique extends to zigzag traversal, right side view, level averages, and more"
|
||
|
||
time_complexity: "O(n). Every node is visited exactly once, and the final reversal is also O(n)."
|
||
space_complexity: "O(n). The queue holds at most one level of nodes (~n/2 in a complete binary tree), and the result stores all n node values."
|
||
|
||
solutions:
|
||
- approach_name: BFS with Reversal
|
||
is_optimal: true
|
||
code: |
|
||
from collections import deque
|
||
|
||
class TreeNode:
|
||
def __init__(self, val=0, left=None, right=None):
|
||
self.val = val
|
||
self.left = left
|
||
self.right = right
|
||
|
||
def level_order_bottom(root: TreeNode | None) -> list[list[int]]:
|
||
# Handle empty tree
|
||
if not root:
|
||
return []
|
||
|
||
result = []
|
||
queue = deque([root])
|
||
|
||
while queue:
|
||
# Capture current level size BEFORE processing
|
||
level_size = len(queue)
|
||
level_values = []
|
||
|
||
# Process exactly level_size nodes (all nodes at current level)
|
||
for _ in range(level_size):
|
||
node = queue.popleft()
|
||
level_values.append(node.val)
|
||
|
||
# Add children for next level
|
||
if node.left:
|
||
queue.append(node.left)
|
||
if node.right:
|
||
queue.append(node.right)
|
||
|
||
# This level is complete
|
||
result.append(level_values)
|
||
|
||
# Reverse to get bottom-up order
|
||
result.reverse()
|
||
return result
|
||
explanation: |
|
||
**Time Complexity:** O(n) — Each node added and removed from queue exactly once, plus O(n) for the reversal.
|
||
|
||
**Space Complexity:** O(n) — Queue can hold up to n/2 nodes, and result stores all n values.
|
||
|
||
Standard BFS processes levels top-to-bottom. We collect each level, then reverse the result at the end. The reversal adds O(n) time but doesn't change the overall complexity.
|
||
|
||
- approach_name: DFS with Level Tracking
|
||
is_optimal: false
|
||
code: |
|
||
def level_order_bottom(root: TreeNode | None) -> list[list[int]]:
|
||
result = []
|
||
|
||
def dfs(node: TreeNode | None, level: int) -> None:
|
||
if not node:
|
||
return
|
||
|
||
# First time at this level? Create new list
|
||
if level == len(result):
|
||
result.append([])
|
||
|
||
# Add current node to its level
|
||
result[level].append(node.val)
|
||
|
||
# Recurse to children (level + 1)
|
||
dfs(node.left, level + 1)
|
||
dfs(node.right, level + 1)
|
||
|
||
dfs(root, 0)
|
||
# Reverse to get bottom-up order
|
||
result.reverse()
|
||
return result
|
||
explanation: |
|
||
**Time Complexity:** O(n) — Visit each node once, plus O(n) for reversal.
|
||
|
||
**Space Complexity:** O(h) — Recursion stack depth equals tree height (O(log n) for balanced, O(n) for skewed).
|
||
|
||
DFS alternative: pass the level as a parameter. Each node appends to its level's list. Left-to-right order is preserved because we recurse left before right. Finally, reverse the result for bottom-up order.
|