DSA sheet · Trees · BFS (level order) pattern

Average of Levels in Binary Tree

Third problem in the level order pattern. Instead of printing each level, we report one number per level: the average of its values. The teacher uses it to teach two habits: don't store what you don't need (keep a running sum instead of a list of values), and read the value range so the sum doesn't overflow. She solves it with BFS (brute force, then optimised), then shows why DFS can't average "on the spot" and fixes that with two small arrays.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

The tree node

given by LeetCode, don't write this in the solution
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

Words we need

plain level order with BFS (the base we will edit)
from collections import deque

class Solution:
    def levelOrder(self, root):
        ans = []
        if root is None:
            return ans
        queue = deque([root])
        while queue:
            size = len(queue)          # nodes on the current level
            level = []
            for i in range(size):
                node = queue.popleft()
                level.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans.append(level)
        return ans

Part A · Brute force BFS

LeetCode 637 · Average of Levels in Binary Tree

1The question in simple words

Given the root of a binary tree, return a list with the average value of the nodes on each level, from top to bottom. Answers within 10−5 of the true value are accepted.

        3          level 0: 3 ÷ 1        = 3.0
       / \
      9   20       level 1: (9+20) ÷ 2   = 14.5
         /  \
        15   7     level 2: (15+7) ÷ 2   = 11.0

Answer: [3.0, 14.5, 11.0].

Doubt: why must the answer be a decimal number?
→ Because averages often aren't whole numbers (29 ÷ 2 = 14.5). "Within 10−5" means the judge looks at about 5 digits after the decimal point, so we need a double (in Python, a float). In Python, / always gives a float, so 29 / 2 is 14.5. (Careful: // would give 14 and be wrong.)

2What the constraints tell us

Doubt: do I need to worry about overflow in Python?
→ No. Python ints grow as big as they need to. But say it in an interview anyway: "values use the full int range, so in Java I'd sum in a long". It shows you read the constraints.

3Intuition

Level order traversal already gives us the rows: [[3], [9, 20], [15, 7]]. Once we have a row, its average is just "add them up, divide by how many". So the first plan is: get all the rows, then average each row.

4Building the logic from the example

  1. Run plain level order → [[3], [9, 20], [15, 7]].
  2. Row [3]: sum 3, count 1 → 3.0.
  3. Row [9, 20]: sum 29, count 2 → 14.5.
  4. Row [15, 7]: sum 22, count 2 → 11.0.

Why it's only brute force

5Approach steps

  1. Put the root in a queue (no empty check, the constraints promise a root).
  2. Do plain level order and collect each level as a list.
  3. For each stored level, append sum(level) / len(level) to the answer.
  4. Return the answer.

6Code (Python)

Brute force: store each level, then average it
from collections import deque

class Solution:
    def averageOfLevels(self, root):
        levels = []
        queue = deque([root])               # constraints: root always exists
        while queue:
            size = len(queue)
            level = []
            for i in range(size):
                node = queue.popleft()
                level.append(node.val)      # store the value (the waste)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            levels.append(level)
        ans = []
        for level in levels:                # second pass
            ans.append(sum(level) / len(level))
        return ans

7Code line by line

linewhat it means
queue = deque([root])Start BFS. No None check needed: at least 1 node.
size = len(queue)The number of nodes on the current level.
level.append(node.val)Keep every value of the level. Part B removes this.
levels.append(level)One finished level stored.
sum(level) / len(level)The average of one row. / gives a float.

8Dry run

  1. BFS gives levels = [[3], [9, 20], [15, 7]] (same queue steps as plain level order).
  2. Second pass: 3/1 = 3.0 → 29/2 = 14.5 → 22/2 = 11.0.
  3. Return [3.0, 14.5, 11.0].

9Complexity & remember

Remember the brute forceLevel order → store rows → average each row. Correct, but stores every value for no reason.

Part B · Optimal BFS: running sum

1The question, and 2 the constraints

Same as Part A: at least 1 node (no empty check in BFS), up to 104 nodes (O(n) needed), and full-int values (sum in a long / float in other languages).

3Intuition: replace the row list with a number

For the average we only need two numbers per level: the sum and the count. The count is already there for free: it's size, the number of nodes in this level. So swap the row list for a single total variable, and add each value to it the moment we pop the node.

4Building the logic: edit the level order code line by line

The teacher literally starts from the level order code and changes it:

level order codewhat happens to itnew code
if root is None: return ansremoved: the constraints promise a root(nothing)
ans is a list of listsnow a list of floats, one per levelans = []
level = []removed: we don't want a sublisttotal = 0
level.append(node.val)add to the sum insteadtotal += node.val
push left / right childunchangedsame
ans.append(level)append the averageans.append(total / size)
Doubt 1: where does total = 0 go, inside or outside the while loop?
→ Inside the while loop, before the for loop. Each level needs its own fresh sum. If you set it once outside, level 1's sum would still contain level 0's 3, and every average after the first would be wrong.
Doubt 2: can I divide by len(queue) after the for loop?
→ No. By then the queue holds the next level's children. Use the size you saved at the start of the round: that's exactly how many values went into total.

5Approach steps

  1. Put the root in the queue.
  2. While the queue isn't empty: size = len(queue), total = 0.
  3. Pop size nodes; add each value to total; push its children.
  4. After the for loop, append total / size.
  5. Return the list of averages.

6Code (Python)

Optimal BFS: running sum per level
from collections import deque

class Solution:
    def averageOfLevels(self, root):
        ans = []
        queue = deque([root])               # constraints: at least 1 node
        while queue:
            size = len(queue)
            total = 0                       # fresh sum for this level
            for i in range(size):
                node = queue.popleft()
                total += node.val           # add instead of storing
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans.append(total / size)        # average of this level
        return ans

7Code line by line

linewhat it means
ans = []One float per level, top to bottom.
size = len(queue)How many nodes are on this level. This is also the count for the average.
total = 0The sum restarts at 0 for every level. (In Java: double or long, because of the int range.)
total += node.valO(1) extra space: one number instead of a list.
queue.append(node.left / right)Same as level order: children wait for the next round.
ans.append(total / size)After the whole level is popped, divide. / makes it a float.

8Dry run on the example

level 03size 1, total 0
pop 3 → total 3 → push 9, 20 → append 3 / 1 = 3.0
level 1920size 2, total 0
pop 9 → total 9 (no children) → pop 20 → total 29 → push 15, 7 → append 29 / 2 = 14.5
level 2157size 2, total 0
pop 15 → total 15 → pop 7 → total 22 → append 22 / 2 = 11.0
endqueue empty → return [3.0, 14.5, 11.0]

9Complexity & remember

         1
       /   \
      2     3
     / \   / \
    4   5 6   7     ← queue holds these 4 at once (≈ n/2)
Remember optimal BFSLevel order, but level = [] becomes total = 0, and ans.append(level) becomes ans.append(total / size). Reset the sum every level.

Part C · DFS with sum and count arrays

1The question

Same question, now with recursion. The teacher shows the idea and the code and asks you to try it yourself. For this part she uses a tree with more nodes on level 2:

         3            level 0
       /   \
      9     20        level 1
     / \   /  \
    4   6 15   7      level 2

Expected: level 0 → 3/1 = 3.0, level 1 → 29/2 = 14.5, level 2 → (4+6+15+7)/4 = 32/4 = 8.0. Answer [3.0, 14.5, 8.0].

2What the constraints tell us

3Intuition: why DFS can't average on the spot

In BFS, all nodes of a level come out together, one after the other. So when the for loop ends, the level is complete and we can divide right away.

DFS goes deep first. On this tree it visits 3 → 9 → 4 → 6 → 20 → 15 → 7. When it's at 9, it doesn't know about 20 yet, which is on the same level. Level 2's nodes arrive at four different moments. We can't divide until the whole traversal is over, so we must remember something per level.

The teacher gives two options:

  1. Do a full level order traversal with DFS (store every value per level), then average each row. That's the Part A waste again.
  2. Better: keep two small arrays, indexed by level: sums[level] and counts[level]. Each node adds its value to its level's sum and adds 1 to its level's count. At the end, divide them.

4Building the logic

Doubt 1: the teacher says "initialize both arrays with zeros". How big, if we don't know the height?
→ Either grow them as you go (what the code below does: a new 0 when a level is reached for the first time), or first compute the tree's height and make arrays of that size. Growing is simpler. It works for the same reason as in zigzag DFS: you can't reach level k without first passing through level k−1, so the arrays are never more than one slot short.
Doubt 2: why not just one array of averages, updated as we go?
→ An average can't be updated from the old average alone; you'd need the old count too. Keeping the raw sum and the count is simpler and exact. We divide only once, at the end.

5Approach steps

  1. Make empty sums and counts; call dfs(root, 0).
  2. In dfs: if the node is None, return.
  3. If level == len(sums), append 0 to both arrays.
  4. Add the value to sums[level], add 1 to counts[level].
  5. Recurse left with level + 1, then right with level + 1.
  6. After the DFS, loop over the levels and append sums[i] / counts[i].

6Code (Python)

DFS with a sum array and a count array
class Solution:
    def averageOfLevels(self, root):
        sums, counts = [], []
        self.dfs(root, 0, sums, counts)
        ans = []
        for i in range(len(sums)):          # one pass over the levels
            ans.append(sums[i] / counts[i])
        return ans

    def dfs(self, root, level, sums, counts):
        if root is None:                    # base case: stops the recursion
            return
        if level == len(sums):              # first node on this level
            sums.append(0)
            counts.append(0)
        sums[level] += root.val
        counts[level] += 1
        self.dfs(root.left, level + 1, sums, counts)
        self.dfs(root.right, level + 1, sums, counts)

7Code line by line

linewhat it means
sums, counts = [], []Index i holds the sum / the number of nodes on level i.
self.dfs(root, 0, sums, counts)Fill both arrays with one DFS, starting at level 0.
if root is None: returnBase case.
if level == len(sums):A level we've never seen → give it a 0 sum and a 0 count.
sums[level] += root.val counts[level] += 1This node joins its level's total and count.
self.dfs(root.left, level + 1, ...)Children are one level deeper.
sums[i] / counts[i]Only now is every level complete, so only now can we divide.

8Dry run: watch the two arrays

visitlevelnew slot?sumscounts
30yes[3][1]
91yes[3, 9][1, 1]
42yes[3, 9, 4][1, 1, 1]
62no[3, 9, 10][1, 1, 2]
201no[3, 29, 10][1, 2, 2]
152no[3, 29, 25][1, 2, 3]
72no[3, 29, 32][1, 2, 4]

Final pass: 3/1 = 3.0, 29/2 = 14.5, 32/4 = 8.0 → [3.0, 14.5, 8.0].

stack when visiting 4
dfs(3, 0)dfs(9, 1)dfs(4, 2)
stack when visiting 20
dfs(3, 0)dfs(20, 1)
Doubt 3: in the video, 6 is called a level-one node at one point. Which level is it?
→ 6 is the right child of 9, so it's on level 2, with 4. Its value goes into sums[2] (4 + 6 = 10) and counts[2] becomes 2. The table above is the corrected trace; the final numbers in the video (32 and 4 for level 2) match it.

9Complexity & remember

Doubt 4: the teacher says the arrays take O(log n) and that DFS "won the race". Is DFS always lighter?
→ Small correction: the arrays have one slot per level, so they're O(height). Height is about log n only for a balanced tree. For a skewed tree (a straight chain) the height is n, so the arrays and the call stack are O(n). Compare that with BFS: its queue holds the widest level, about n/2 for a full tree, but just 1 for a chain. So DFS uses less memory on bushy, balanced trees, and BFS uses less on long thin ones. Time is O(n) for both. On LeetCode's tests DFS happened to come out ahead.
Remember DFS averagesDFS can't divide during the walk, because a level's nodes arrive at different times. Keep sums[level] and counts[level], add a slot when level == len(sums), divide at the end.

Part D · Revision page

A · BFS bruteB · BFS running sumC · DFS two arrays
per level we keepa list of all valuesone number totala slot in sums and counts
count of a levellen(level)size (saved before the loop)counts[level]
when we divideafter the whole BFSright after each levelafter the whole DFS
empty-tree checknot needed (≥ 1 node)not needed (≥ 1 node)base case always needed
timeO(2n)O(n)O(n) + O(height)
extra spaceO(n) queue + O(n) valuesO(n) queue (widest level)O(height): arrays + call stack
If you remember only 5 lines 1. Average per level = sum of that level ÷ number of nodes on it.
2. BFS: total = 0 per level, total += node.val, append total / size.
3. Use the saved size, not len(queue) after the loop.
4. Values span the full int range: sum in a long (Java/C++); Python is safe.
5. DFS: sums[level] and counts[level], divide at the end.
Mistakes to avoid ✗ setting total = 0 once outside the while loop
✗ using // (integer division) instead of /
✗ summing in an int in Java/C++ (overflow)
✗ trying to average inside DFS before the walk is over
✗ dropping the DFS base case because "the root always exists"
test it yourself (paste under any of the solutions above)
t1 = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7)))
t2 = TreeNode(3, TreeNode(9, TreeNode(4), TreeNode(6)), TreeNode(20, TreeNode(15), TreeNode(7)))
big = TreeNode(2147483647, TreeNode(2147483647), TreeNode(2147483647))

s = Solution()
print(s.averageOfLevels(t1))     # [3.0, 14.5, 11.0]
print(s.averageOfLevels(t2))     # [3.0, 14.5, 8.0]
print(s.averageOfLevels(big))    # [2147483647.0, 2147483647.0]

Based on this video: Average of Levels in Binary Tree | BFS & DFS