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.

What you’ll learn

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

The cue

The template

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
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.

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.

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

For the tree above:

Outer iterationQueue at snapshotlevel_widthlevel_widthLevel 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

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.

The variant map

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 dequedeque and appendleftappendleft on odd levels103 Zigzag Level Order
Early exitreturnreturn mid-traversal on the first node meeting a condition111 Minimum Depth
Carry extra state per nodeEnqueue (node, depth)(node, depth) or (node, index)(node, index) tuples instead of bare nodes662 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 O(n)O(n) — each node is enqueued once and dequeued once. Space O(w)O(w) 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 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.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 O(n)O(n). Space O(w)O(w).

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 O(1)O(1), so the whole thing stays O(n)O(n). Building a list and calling level.insert(0, val)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(...)level.append(...) then level.reverse()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 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 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)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.

LeetCode problem set

#ProblemDifficultyThe twist
111Minimum Depth of Binary TreeEasyEarly exit on the first leaf; one-child nodes are not leaves
637Average of Levels in Binary TreeEasyThe template with sum(level) / len(level)sum(level) / len(level)
993Cousins in Binary TreeEasySame depth, different parent — track the parent per node
101Symmetric TreeEasyCompare each level against its own reverse (or two-pointer the queue)
102Binary Tree Level Order TraversalMediumThe base template
107Binary Tree Level Order Traversal IIMediumReverse the result, never the traversal
199Binary Tree Right Side ViewMediumKeep the last node per level
515Find Largest Value in Each Tree RowMediummax()max() per level
103Binary Tree Zigzag Level Order TraversalMediumFlip the assembly order, not the traversal
1161Maximum Level Sum of a Binary TreeMediumTrack the level index of the best sum (1-indexed)
1302Deepest Leaves SumMediumKeep overwriting one running sum; the last level wins
116Populating Next Right Pointers in Each NodeMediumPerfect tree — do it in O(1)O(1) space using the level you just linked
117Populating Next Right Pointers in Each Node IIMediumArbitrary tree — the dummy-head trick per level
958Check Completeness of a Binary TreeMediumEnqueue NoneNones too; no real node may follow a NoneNone
662Maximum Width of Binary TreeMediumEnqueue (node, index)(node, index); width is last - first + 1last - first + 1
863All Nodes Distance K in Binary TreeMediumAdd parent links first, then BFS the tree as an undirected graph
314Binary Tree Vertical Order TraversalMedium · PremiumEnqueue (node, column)(node, column) and bucket by column

Interview follow-ups

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)list.pop(0) is O(n)O(n) and makes the traversal O(n2)O(n^2); deque.popleft()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 nextnext pointers to link the level below; no queue needed
“Do it recursively”FlexibilityPass 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 limitsBFS holds a whole level in memory; if ww is too large, iterative-deepening DFS trades time for O(depth)O(\text{depth}) space

Edge-case checklist

  • Empty tree (root is Noneroot is None) — must return [][] / 00, and must not enqueue NoneNone.
  • 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 or float("-inf")float("-inf").
  • Very wide tree — mention the O(w)O(w) 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.
  • O(n)O(n) time, O(w)O(w) space — and deque.popleft()deque.popleft(), never list.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 coffee

Was this page helpful?

Let us know how we did