219 lines
9.0 KiB
YAML
219 lines
9.0 KiB
YAML
title: Average of Levels in Binary Tree
|
|
slug: average-of-levels-in-binary-tree
|
|
difficulty: easy
|
|
leetcode_id: 637
|
|
leetcode_url: https://leetcode.com/problems/average-of-levels-in-binary-tree/
|
|
categories:
|
|
- trees
|
|
- queue
|
|
patterns:
|
|
- bfs
|
|
- tree-traversal
|
|
|
|
function_signature: "def average_of_levels(root: TreeNode) -> list[float]:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { root: [3, 9, 20, null, null, 15, 7] }
|
|
expected: [3.0, 14.5, 11.0]
|
|
- input: { root: [3, 9, 20, 15, 7] }
|
|
expected: [3.0, 14.5, 11.0]
|
|
hidden:
|
|
- input: { root: [1] }
|
|
expected: [1.0]
|
|
- input: { root: [1, 2, 3, 4, 5, 6, 7] }
|
|
expected: [1.0, 2.5, 5.5]
|
|
- input: { root: [1, 2, null, 3, null, 4] }
|
|
expected: [1.0, 2.0, 3.0, 4.0]
|
|
- input: { root: [0, -1, 1] }
|
|
expected: [0.0, 0.0]
|
|
|
|
description: |
|
|
Given the `root` of a binary tree, return *the average value of the nodes on each level in the form of an array*.
|
|
|
|
Answers within `10^-5` of the actual answer will be accepted.
|
|
|
|
constraints: |
|
|
- The number of nodes in the tree is in the range `[1, 10^4]`
|
|
- `-2^31 <= Node.val <= 2^31 - 1`
|
|
|
|
examples:
|
|
- input: "root = [3,9,20,null,null,15,7]"
|
|
output: "[3.00000,14.50000,11.00000]"
|
|
explanation: "The average value of nodes on level 0 is 3, on level 1 is 14.5, and on level 2 is 11. Hence return [3, 14.5, 11]."
|
|
- input: "root = [3,9,20,15,7]"
|
|
output: "[3.00000,14.50000,11.00000]"
|
|
explanation: "The tree has three levels with averages 3, 14.5, and 11 respectively."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine the binary tree as floors of a building, where each level represents a floor. Your task is to walk through each floor, count all the people (node values), and calculate the average number per floor.
|
|
|
|
The key insight is that we need to **process the tree level by level**, keeping nodes from the same depth together. This naturally maps to **Breadth-First Search (BFS)**, which explores all nodes at the current depth before moving to the next level.
|
|
|
|
Think of it like this: we use a queue as a "waiting room." We start with the root on floor 0. Then, for each floor, we let everyone in the waiting room step forward (process them), calculate their average, and have them send their children to the waiting room for the next floor. We repeat until no one is left waiting.
|
|
|
|
The queue maintains the level-by-level ordering that makes this problem straightforward.
|
|
|
|
approach: |
|
|
We solve this using **Breadth-First Search (BFS)** with a queue:
|
|
|
|
**Step 1: Handle the edge case**
|
|
|
|
- If `root` is `None`, return an empty list
|
|
|
|
|
|
|
|
**Step 2: Initialise data structures**
|
|
|
|
- `result`: An empty list to store the average of each level
|
|
- `queue`: A deque initialised with the root node
|
|
|
|
|
|
|
|
**Step 3: Process the tree level by level**
|
|
|
|
- While the queue is not empty:
|
|
- Record the current `level_size` (number of nodes at this level)
|
|
- Initialise `level_sum` to `0` for accumulating node values
|
|
- Process exactly `level_size` nodes:
|
|
- Dequeue a node from the front
|
|
- Add its value to `level_sum`
|
|
- Enqueue its left child if it exists
|
|
- Enqueue its right child if it exists
|
|
- Calculate the average: `level_sum / level_size`
|
|
- Append the average to `result`
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- Return `result` containing the average for each level
|
|
|
|
|
|
|
|
This approach ensures we process all nodes at depth `d` before any node at depth `d+1`, giving us clean level-by-level averages.
|
|
|
|
common_pitfalls:
|
|
- title: Integer Overflow
|
|
description: |
|
|
Node values can range from `-2^31` to `2^31 - 1`. When summing values at a level with many nodes, the sum could overflow 32-bit integers.
|
|
|
|
In Python, this isn't an issue due to arbitrary-precision integers. In languages like Java or C++, you should use `long` for the sum or cast to `double` early.
|
|
wrong_approach: "Using int for level_sum in languages with fixed-width integers"
|
|
correct_approach: "Use long or double for accumulating sums"
|
|
|
|
- title: Using DFS Without Level Tracking
|
|
description: |
|
|
While DFS can solve this problem, it requires extra bookkeeping to track which level each node belongs to. A common mistake is to use DFS and lose track of the level structure.
|
|
|
|
BFS naturally groups nodes by level, making it the more intuitive approach for this problem.
|
|
wrong_approach: "DFS without proper level indexing"
|
|
correct_approach: "BFS with queue processing level_size nodes at a time"
|
|
|
|
- title: Forgetting to Snapshot Level Size
|
|
description: |
|
|
When processing a level, you must capture the queue size **before** you start dequeuing. If you check `len(queue)` inside the loop, you'll include children you just added.
|
|
|
|
For example, with `[3,9,20]`, if you don't snapshot the size, processing the root adds `9` and `20` to the queue, and you'd process them in the same iteration.
|
|
wrong_approach: "Looping while queue has elements without fixed level size"
|
|
correct_approach: "Capture level_size = len(queue) before the inner loop"
|
|
|
|
key_takeaways:
|
|
- "**BFS for level-order problems**: When you need to process a tree level by level, BFS with a queue is the natural choice"
|
|
- "**Snapshot the level size**: Always capture `len(queue)` before processing to avoid mixing levels"
|
|
- "**Foundation for similar problems**: This pattern applies to many tree problems — level order traversal, right side view, zigzag traversal, and more"
|
|
- "**DFS alternative**: DFS works too if you pass the level index and use a list indexed by level, but BFS is more intuitive here"
|
|
|
|
time_complexity: "O(n). We visit each of the `n` nodes in the tree exactly once."
|
|
space_complexity: "O(w) where `w` is the maximum width of the tree. In the worst case (a complete binary tree), the last level has roughly `n/2` nodes, so this is O(n)."
|
|
|
|
solutions:
|
|
- approach_name: BFS with Queue
|
|
is_optimal: true
|
|
code: |
|
|
from collections import deque
|
|
from typing import Optional
|
|
|
|
class TreeNode:
|
|
def __init__(self, val=0, left=None, right=None):
|
|
self.val = val
|
|
self.left = left
|
|
self.right = right
|
|
|
|
def average_of_levels(root: Optional[TreeNode]) -> list[float]:
|
|
if not root:
|
|
return []
|
|
|
|
result = []
|
|
queue = deque([root])
|
|
|
|
while queue:
|
|
# Snapshot the number of nodes at this level
|
|
level_size = len(queue)
|
|
level_sum = 0
|
|
|
|
# Process all nodes at the current level
|
|
for _ in range(level_size):
|
|
node = queue.popleft()
|
|
level_sum += node.val
|
|
|
|
# Add children to queue for next level
|
|
if node.left:
|
|
queue.append(node.left)
|
|
if node.right:
|
|
queue.append(node.right)
|
|
|
|
# Calculate and store the average for this level
|
|
result.append(level_sum / level_size)
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each node is visited exactly once.
|
|
|
|
**Space Complexity:** O(w) — The queue holds at most one level of nodes at a time. The maximum width `w` can be up to `n/2` for a complete binary tree.
|
|
|
|
BFS naturally processes nodes level by level. By snapshotting the queue size before processing, we ensure each level is handled independently, making the average calculation straightforward.
|
|
|
|
- approach_name: DFS with Level Tracking
|
|
is_optimal: false
|
|
code: |
|
|
from typing import Optional
|
|
|
|
class TreeNode:
|
|
def __init__(self, val=0, left=None, right=None):
|
|
self.val = val
|
|
self.left = left
|
|
self.right = right
|
|
|
|
def average_of_levels(root: Optional[TreeNode]) -> list[float]:
|
|
# Each entry: [sum_of_values, count_of_nodes]
|
|
level_data = []
|
|
|
|
def dfs(node: Optional[TreeNode], level: int) -> None:
|
|
if not node:
|
|
return
|
|
|
|
# Expand the list if we've reached a new level
|
|
if level >= len(level_data):
|
|
level_data.append([0, 0])
|
|
|
|
# Accumulate sum and count for this level
|
|
level_data[level][0] += node.val
|
|
level_data[level][1] += 1
|
|
|
|
# Recurse to children at the next level
|
|
dfs(node.left, level + 1)
|
|
dfs(node.right, level + 1)
|
|
|
|
dfs(root, 0)
|
|
|
|
# Convert accumulated data to averages
|
|
return [total / count for total, count in level_data]
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each node is visited exactly once.
|
|
|
|
**Space Complexity:** O(h) for recursion stack where `h` is the tree height, plus O(h) for `level_data`. In the worst case (skewed tree), `h = n`.
|
|
|
|
DFS can also solve this problem by tracking the level during traversal. We accumulate sums and counts in a list indexed by level, then compute averages at the end. While this works, it's less intuitive than BFS for level-based problems.
|