Tree BFS and Level Order
Any tree question containing the words “level”, “depth”, “row”, “nearest”, or “width” is a breadth-first traversal in disguise. DFS visits a whole branch before its siblings, so it never has a level assembled in one place; BFS processes the tree in horizontal layers, which is exactly the shape those questions ask about.
There is one template. Once you can write it without thinking, roughly a dozen Medium problems become five-minute problems.
What you’ll learn
- The level-snapshot trick — the single line that turns a flat queue into “one iteration per level”.
- Why
collections.dequecollections.dequeand not a list (and what it costs if you get it wrong). - The five mutations of the template that cover the whole level-order family.
- Four real LeetCode problems solved here in the browser: 102, 199, 103, and 111.
The cue
The template
from collections import deque
def level_order(root):
if not root: # always handle the empty tree first
return []
result = []
queue = deque([root])
while queue:
level_width = len(queue) # <-- THE snapshot: freeze this level's size
level = []
for _ in range(level_width): # consume exactly this level
node = queue.popleft()
level.append(node.val)
if node.left: # children form the NEXT level
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return resultfrom collections import deque
def level_order(root):
if not root: # always handle the empty tree first
return []
result = []
queue = deque([root])
while queue:
level_width = len(queue) # <-- THE snapshot: freeze this level's size
level = []
for _ in range(level_width): # consume exactly this level
node = queue.popleft()
level.append(node.val)
if node.left: # children form the NEXT level
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return resultEverything in the level-order family is this template with one of the inner lines changed.
How it works
Level by level, the queue holds exactly one complete level at the top of
each whilewhile iteration. The inner forfor drains that level and, in the
process, enqueues the entire next level behind it.
graph TD
A["3"] --> B["9"]
A --> C["20"]
C --> D["15"]
C --> E["7"]
For the tree above:
| Outer iteration | Queue at snapshot | level_widthlevel_width | Level emitted |
|---|---|---|---|
| 1 | [3][3] | 1 | [3][3] |
| 2 | [9, 20][9, 20] | 2 | [9, 20][9, 20] |
| 3 | [15, 7][15, 7] | 2 | [15, 7][15, 7] |
| 4 | [][] | — | loop ends |
Time and space complexity
| Time | Space | |
|---|---|---|
| Level-order BFS | — every node enqueued and dequeued once | where is the maximum level width |
For a balanced tree the widest level holds about nodes, so space is . For a degenerate “linked list” tree the queue never holds more than one node, so space is — the exact opposite of DFS recursion, which is on a balanced tree and on a degenerate one.
The variant map
| Variant | What changes in the template | Canonical problem |
|---|---|---|
| Collect every level | The template verbatim | 102 Binary Tree Level Order Traversal |
| One value per level | Keep only the last (or first, max, mean) node of each level | 199 Right Side View · 515 Largest Value · 637 Average |
| Alternate direction | Build the level in a dequedeque and appendleftappendleft on odd levels | 103 Zigzag Level Order |
| Early exit | returnreturn mid-traversal on the first node meeting a condition | 111 Minimum Depth |
| Carry extra state per node | Enqueue (node, depth)(node, depth) or (node, index)(node, index) tuples instead of bare nodes | 662 Maximum Width · 993 Cousins |
Practice — real LeetCode problems
Each exercise is the real LeetCode problem with its real method signature.
TreeNodeTreeNode and a build(...)build(...) helper (which turns LeetCode’s level-order
array notation into an actual tree) are provided in the editor. Write the
body, press Run, and match the expected output.
LC 102 — Binary Tree Level Order Traversal · Medium
Problem. Given the rootroot of a binary tree, return the values of its
nodes grouped by level, from left to right, one list per level.
Constraints. 0 <= number of nodes <= 20000 <= number of nodes <= 2000,
-1000 <= Node.val <= 1000-1000 <= Node.val <= 1000.
Examples. [3,9,20,null,null,15,7][3,9,20,null,null,15,7] gives [[3],[9,20],[15,7]][[3],[9,20],[15,7]] ·
[1][1] gives [[1]][[1]] · [][] gives [][]
Editorial — approach, complexity, follow-ups
Push the root, then repeat: read how many nodes are in the queue (that is one full level), pop exactly that many, record their values, and enqueue their children.
Time — each node is enqueued once and dequeued once.
Space for the queue, where ww is the widest level.
The empty-tree guard is not optional: [][] must return [][], and without
the guard deque([None])deque([None]) would enqueue a NoneNone and crash on
node.valnode.val.
Follow-ups you should expect: “Bottom-up (LC 107)?” — return out[::-1]return out[::-1]. “Can you do it with DFS?” — yes: recurse with a depthdepth
argument and append to out[depth]out[depth], creating the list when
depth == len(out)depth == len(out). It is too, and worth mentioning as it shows
you understand the traversals are interchangeable here; BFS is just the
natural fit. “What about an N-ary tree (LC 429)?” — replace the two
if node.left / node.rightif node.left / node.right lines with for child in node.childrenfor child in node.children.
LC 199 — Binary Tree Right Side View · Medium
Problem. Given the rootroot of a binary tree, imagine standing to its
right. Return the values of the nodes you can see, ordered top to bottom.
Constraints. 0 <= number of nodes <= 1000 <= number of nodes <= 100.
Examples. [1,2,3,null,5,null,4][1,2,3,null,5,null,4] gives [1,3,4][1,3,4] ·
[1,null,3][1,null,3] gives [1,3][1,3] · [][] gives [][]
Editorial — approach, complexity, follow-ups
The visible node on each level is simply its last node in left-to-right
order. So run the level template and record only the node at index
width - 1width - 1.
Time . Space .
Follow-ups you should expect: “Left side view?” — record index 00
instead. “Do it with DFS?” — visit right child first and append when
depth == len(out)depth == len(out); that records the first node seen at each new depth,
which is the rightmost. “Largest value per level (LC 515)?” — same shape,
max()max() over the level instead of the last element.
LC 103 — Binary Tree Zigzag Level Order Traversal · Medium
Problem. Return the level-order traversal, but alternate direction per level: level 0 left-to-right, level 1 right-to-left, level 2 left-to-right, and so on.
Constraints. 0 <= number of nodes <= 20000 <= number of nodes <= 2000.
Examples. [3,9,20,null,null,15,7][3,9,20,null,null,15,7] gives [[3],[20,9],[15,7]][[3],[20,9],[15,7]] ·
[1][1] gives [[1]][[1]] · [][] gives [][]
Editorial — approach, complexity, follow-ups
The traversal never changes. Only the assembly of each level flips: for right-to-left levels, prepend each value instead of appending.
Using a dequedeque for the level makes prepending , so the whole thing
stays . Building a list and calling level.insert(0, val)level.insert(0, val) is
per insert and makes the level assembly
— a legitimate thing for an interviewer to poke at.
level.append(...)level.append(...) then level.reverse()level.reverse() on alternate levels is equally
overall and just as acceptable. What is not acceptable is trying
to enqueue children in a different order to “naturally” produce zigzag —
the direction flips every level, so child ordering cannot express it.
Time . Space .
Follow-ups you should expect: “Spiral starting right-to-left?” —
initialise the flag to FalseFalse. “Vertical order traversal (LC 314)?” —
different pattern: enqueue (node, column)(node, column) and bucket by column.
LC 111 — Minimum Depth of Binary Tree · Easy
Problem. Return the number of nodes along the shortest path from the root down to the nearest leaf. A leaf is a node with no children.
Constraints. 0 <= number of nodes <= 10^50 <= number of nodes <= 10^5.
Examples. [3,9,20,null,null,15,7][3,9,20,null,null,15,7] gives 22 ·
[2,null,3,null,4,null,5,null,6][2,null,3,null,4,null,5,null,6] gives 55 · [][] gives 00
Editorial — approach, complexity, follow-ups
BFS dequeues nodes in non-decreasing depth order, so the first leaf it meets is a shallowest leaf. Return immediately — no need to finish the traversal.
Time worst case, but only in practice. On a tree with one short branch and a million-node deep branch, BFS returns almost instantly while a full DFS walks everything. Space .
Follow-ups you should expect: “Maximum depth (LC 104)?” — switch to
DFS, 1 + max(left, right)1 + max(left, right); there is no early exit to win so recursion is
clearer. “Why not DFS here?” — the early exit; say it explicitly. “What
if the tree is enormous and wide?” — BFS memory is which can be
huge; an iterative-deepening DFS trades time for space.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 111 | Minimum Depth of Binary Tree | Easy | Early exit on the first leaf; one-child nodes are not leaves |
| 637 | Average of Levels in Binary Tree | Easy | The template with sum(level) / len(level)sum(level) / len(level) |
| 993 | Cousins in Binary Tree | Easy | Same depth, different parent — track the parent per node |
| 101 | Symmetric Tree | Easy | Compare each level against its own reverse (or two-pointer the queue) |
| 102 | Binary Tree Level Order Traversal | Medium | The base template |
| 107 | Binary Tree Level Order Traversal II | Medium | Reverse the result, never the traversal |
| 199 | Binary Tree Right Side View | Medium | Keep the last node per level |
| 515 | Find Largest Value in Each Tree Row | Medium | max()max() per level |
| 103 | Binary Tree Zigzag Level Order Traversal | Medium | Flip the assembly order, not the traversal |
| 1161 | Maximum Level Sum of a Binary Tree | Medium | Track the level index of the best sum (1-indexed) |
| 1302 | Deepest Leaves Sum | Medium | Keep overwriting one running sum; the last level wins |
| 116 | Populating Next Right Pointers in Each Node | Medium | Perfect tree — do it in space using the level you just linked |
| 117 | Populating Next Right Pointers in Each Node II | Medium | Arbitrary tree — the dummy-head trick per level |
| 958 | Check Completeness of a Binary Tree | Medium | Enqueue NoneNones too; no real node may follow a NoneNone |
| 662 | Maximum Width of Binary Tree | Medium | Enqueue (node, index)(node, index); width is last - first + 1last - first + 1 |
| 863 | All Nodes Distance K in Binary Tree | Medium | Add parent links first, then BFS the tree as an undirected graph |
| 314 | Binary Tree Vertical Order Traversal | Medium · Premium | Enqueue (node, column)(node, column) and bucket by column |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “BFS or DFS here, and why?” | Whether you choose deliberately | Per-level or nearest-thing means BFS; path/subtree/return-value-upward means DFS |
| “What’s the space complexity?” | Precision | = widest level, which is balanced and degenerate — the mirror image of DFS recursion |
| “Why deque and not a list?” | Python fluency | list.pop(0)list.pop(0) is and makes the traversal ; deque.popleft()deque.popleft() is |
| “Can you do 116 in space?” | Whether you can drop the queue | Yes — once a level is linked, walk it via nextnext pointers to link the level below; no queue needed |
| “Do it recursively” | Flexibility | Pass depthdepth down and index into out[depth]out[depth], appending a new list when depth == len(out)depth == len(out) |
| “The tree has a billion nodes” | Practical limits | BFS holds a whole level in memory; if ww is too large, iterative-deepening DFS trades time for space |
Edge-case checklist
- Empty tree (
root is Noneroot is None) — must return[][]/00, and must not enqueueNoneNone. - Single node — one level, and it is also a leaf. LC 111 must return
11. - One-child nodes — the killer for LC 111: a node with one child is not a leaf.
- Completely one-sided tree (
[1,null,2,null,3][1,null,2,null,3]) — every level has width 1; verifies your snapshot logic doesn’t assume two children. - Duplicate values — never identify nodes by
valval; use the node objects themselves. - Negative values — don’t initialise a per-level max to
00; use the first element orfloat("-inf")float("-inf"). - Very wide tree — mention the queue memory before the interviewer does.
Recap
- Tree BFS = a
dequedeque, plus one snapshot line (level_width = len(queue)level_width = len(queue)) that turns a flat queue into per-level iteration. - The whole level-order family is that template with one inner line changed: keep all values, keep one value, flip the assembly order, exit early, or carry extra state per node.
- BFS for “per level” and “nearest”; DFS for paths and subtree properties. Minimum depth is BFS (early exit); maximum depth is DFS.
- time, space — and
deque.popleft()deque.popleft(), neverlist.pop(0)list.pop(0). - To reverse or zigzag, change the assembly, never the traversal.
Next: BST Patterns — what changes once the tree is ordered, and why an in-order walk is the only tool you need for a surprising number of problems.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
