Skip to content

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.

  • The level-snapshot trick — the single line that turns a flat queue into “one iteration per level”.
  • Why collections.deque and 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.
tree_bfs_template.py
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 result

Everything in the level-order family is this template with one of the inner lines changed.

Level by level, the queue holds exactly one complete level at the top of each while iteration. The inner for drains that level and, in the process, enqueues the entire next level behind it.

diagram BFS consumes the queue one full level per outer iteration mermaid

Step through it with the queue on screen. The claim to verify for yourself: at the top of every outer iteration the queue contains exactly one complete level and nothing else. That is not obvious, and it is the only reason the snapshot line works.

treeBFS drains one whole level per outer iterationLC 102 · O(n) time, O(w) space
9315207
queue
3
queue1
seedSeed the queue with the root. BFS uses a queue (first in, first out) — swap it for a stack and you get DFS instead.
1/10

Pause at each 'Level N starts' frame and read the queue. Its length there IS the level width -- which is why len(queue) must be snapshotted before the inner loop, never read inside it.

Same tree, written out the way you should write it on a whiteboard:

Outer iterationQueue at snapshotlevel_widthLevel emitted
1[3]1[3]
2[9, 20]2[9, 20]
3[15, 7]2[15, 7]
4[]loop ends

Now the failure mode, so you recognise it instantly. Suppose the inner loop reads len(queue) live instead of snapshotting it:

Outer iterationQueuerange(len(queue)) re-read each stepLevel emitted
1[3]grows to 2 as 9 and 20 are enqueued[3, 9, 20] ← wrong

One level bleeds into the next, and the output is a single flat list rather than a list of levels. If your LC 102 submission returns [[3,9,20,15,7]], this is why.

TimeSpace
Level-order BFSO(n)O(n) — every node enqueued and dequeued onceO(w)O(w) where ww is the maximum level width

For a balanced tree the widest level holds about n/2n/2 nodes, so space is O(n)O(n). For a degenerate “linked list” tree the queue never holds more than one node, so space is O(1)O(1) — the exact opposite of DFS recursion, which is O(logn)O(\log n) on a balanced tree and O(n)O(n) on a degenerate one.

VariantWhat changes in the templateCanonical problem
Collect every levelThe template verbatim102 Binary Tree Level Order Traversal
One value per levelKeep only the last (or first, max, mean) node of each level199 Right Side View · 515 Largest Value · 637 Average
Alternate directionBuild the level in a deque and appendleft on odd levels103 Zigzag Level Order
Early exitreturn mid-traversal on the first node meeting a condition111 Minimum Depth
Carry extra state per nodeEnqueue (node, depth) or (node, index) tuples instead of bare nodes662 Maximum Width · 993 Cousins

The “one value per level” variant is worth watching, because the trick is smaller than people expect — there is no geometry involved at all:

treeRight side view is just 'the last node popped from each level'LC 199 · one line changed
25134
queue
23
output1
i0 of 0last?yesview1
level 01 is the last node dequeued from level 0, so it is the one you would see standing to the right of the tree. Record it.
1/6

Nothing about x-coordinates or rightmost children. The node you see from the right is whichever node happens to leave the queue last on its level -- so the only change to the template is an `if i == n - 1` test.

Each exercise is the real LeetCode problem with its real method signature. TreeNode and a 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

Section titled “LC 102 — Binary Tree Level Order Traversal · Medium”

Problem. Given the root 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 <= 2000, -1000 <= Node.val <= 1000.

Examples. [3,9,20,null,null,15,7] gives [[3],[9,20],[15,7]] · [1] gives [[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 O(n)O(n) — each node is enqueued once and dequeued once. Space O(w)O(w) for the queue, where w is the widest level.

The empty-tree guard is not optional: [] must return [], and without the guard deque([None]) would enqueue a None and crash on node.val.

Follow-ups you should expect: “Bottom-up (LC 107)?” — return out[::-1]. “Can you do it with DFS?” — yes: recurse with a depth argument and append to out[depth], creating the list when depth == len(out). It is O(n)O(n) 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.right lines with for child in node.children.

LC 199 — Binary Tree Right Side View · Medium

Section titled “LC 199 — Binary Tree Right Side View · Medium”

Problem. Given the root 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 <= 100.

Examples. [1,2,3,null,5,null,4] gives [1,3,4] · [1,null,3] gives [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 - 1.

Time O(n)O(n). Space O(w)O(w).

Follow-ups you should expect: “Left side view?” — record index 0 instead. “Do it with DFS?” — visit right child first and append when 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() over the level instead of the last element.

LC 103 — Binary Tree Zigzag Level Order Traversal · Medium

Section titled “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 <= 2000.

Examples. [3,9,20,null,null,15,7] gives [[3],[20,9],[15,7]] · [1] gives [[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 deque for the level makes prepending O(1)O(1), so the whole thing stays O(n)O(n). Building a list and calling level.insert(0, val) is O(level width)O(\text{level width}) per insert and makes the level assembly O(w2)O(w^2) — a legitimate thing for an interviewer to poke at.

level.append(...) then level.reverse() on alternate levels is equally O(n)O(n) 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 O(n)O(n). Space O(w)O(w).

Follow-ups you should expect: “Spiral starting right-to-left?” — initialise the flag to False. “Vertical order traversal (LC 314)?” — different pattern: enqueue (node, column) and bucket by column.

LC 111 — Minimum Depth of Binary Tree · Easy

Section titled “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^5.

Examples. [3,9,20,null,null,15,7] gives 2 · [2,null,3,null,4,null,5,null,6] gives 5 · [] gives 0

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 O(n)O(n) worst case, but only O(nodes above the shallowest leaf)O(\text{nodes above the shallowest leaf}) 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 O(w)O(w).

Follow-ups you should expect: “Maximum depth (LC 104)?” — switch to DFS, 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 O(w)O(w) which can be huge; an iterative-deepening DFS trades time for O(depth)O(\text{depth}) space.

Generated from the problem database, so every entry carries its sheet membership and reported companies. Progress is saved in this browser.

17 problems
4 easy13 medium0 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

They askWhat they’re checkingThe answer
“BFS or DFS here, and why?”Whether you choose deliberatelyPer-level or nearest-thing means BFS; path/subtree/return-value-upward means DFS
“What’s the space complexity?”PrecisionO(w)O(w) = widest level, which is O(n)O(n) balanced and O(1)O(1) degenerate — the mirror image of DFS recursion
“Why deque and not a list?”Python fluencylist.pop(0) is O(n)O(n) and makes the traversal O(n2)O(n^2); deque.popleft() is O(1)O(1)
“Can you do 116 in O(1)O(1) space?”Whether you can drop the queueYes — once a level is linked, walk it via next pointers to link the level below; no queue needed
“Do it recursively”FlexibilityPass depth down and index into out[depth], appending a new list when depth == len(out)
“The tree has a billion nodes”Practical limitsBFS holds a whole level in memory; if w is too large, iterative-deepening DFS trades time for O(depth)O(\text{depth}) space
  • Empty tree (root is None) — must return [] / 0, and must not enqueue None.
  • Single node — one level, and it is also a leaf. LC 111 must return 1.
  • 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]) — every level has width 1; verifies your snapshot logic doesn’t assume two children.
  • Duplicate values — never identify nodes by val; use the node objects themselves.
  • Negative values — don’t initialise a per-level max to 0; use the first element or float("-inf").
  • Very wide tree — mention the O(w)O(w) queue memory before the interviewer does.
pch.quizTag Tree BFS — self-check
  1. Why must len(queue) be captured before the inner loop rather than read inside it?

    pch.quizShowAnswer

    B — Because the inner loop appends the next level to the same queue, so a live read would let levels bleed together — The inner loop is simultaneously draining this level and filling the next one into the same container. Snapshotting the length freezes the boundary. Read it live and LC 102 returns one flat list instead of a list of levels.

  2. Minimum depth of a binary tree: BFS or DFS?

    pch.quizShowAnswer

    B — BFS — it can stop at the first leaf it meets, and no leaf can be shallower — BFS visits in non-decreasing depth order, so the first leaf found is the shallowest — a genuine early exit on a wide, shallow tree. And note that naive `1 + min(left, right)` DFS is outright WRONG for minimum depth: a node with one child would report a depth through its missing child.

  3. What is the space complexity of level-order BFS, and when is it worst?

    pch.quizShowAnswer

    C — O(w) where w is the widest level — so O(n) on a balanced tree and O(1) on a degenerate one — The queue holds at most one level, so space tracks the maximum width. That is the exact mirror image of DFS recursion, which is O(log n) balanced and O(n) degenerate. Being able to state both directions is the discriminator here.

  4. You need bottom-up level order (LC 107). What is the right move?

    pch.quizShowAnswer

    C — Run the normal template and reverse the result list — Change the assembly, never the traversal. `return result[::-1]` is O(levels) and obviously correct; fighting the traversal order is neither. The same principle covers zigzag — build each level normally, flip alternate ones.

  5. Why deque rather than a plain list as the queue?

    pch.quizShowAnswer

    B — list.pop(0) is O(n) because every remaining element shifts, turning the traversal into O(n²) — This is a Python-fluency check that costs real submissions. A list-as-queue passes the samples and TLEs on the full tests. collections.deque.popleft() is O(1).

  • Cue — the words level, row, depth, nearest, width, or “connect each node to its right neighbour”.
  • Invariant — at the top of each outer iteration, the queue holds exactly one complete level.
  • Templatedeque, while queue:, snapshot n = len(queue), inner for _ in range(n) that pops one node and pushes its children.
  • ComplexityO(n)O(n) time, O(w)O(w) space (widest level). Mirror image of DFS’s O(h)O(h).
  • Choose DFS instead when — the answer is a root-to-leaf path, a subtree property, or anything returned up from children. Minimum depth is BFS; maximum depth is DFS.
  • Tree BFS = a deque, plus one snapshot line (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.
  • O(n)O(n) time, O(w)O(w) space — and deque.popleft(), never 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.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading