Tap nodes to discover which level they belong to.
BFS visits nodes closest to the root first. You already know that. But here is something that catches people off guard: a plain BFS queue does not tell you where one level ends and the next begins.
Think about it. You dequeue a node, enqueue its children, dequeue another node, enqueue more children. The queue is a flat stream of nodes. Nothing in that stream says “level 1 stops here, level 2 begins.” And yet a huge family of tree problems -- level averages, right-side view, zigzag traversal, largest value per level -- all need exactly that grouping: [[1], [2, 3], [4, 5, 6, 7]].
So how do you carve level boundaries out of a flat queue? The entire technique hinges on a single line of code placed before the inner loop. One line that freezes a moving target. Without it, the loop bound shifts under your feet and the levels bleed together.
The next screen shows you exactly what goes wrong when you skip that line. Pay attention to the moment the levels stop being levels.
Here is the most natural way to write the inner loop: for (let i = 0; i < queue.length; i++). It looks correct. The queue has 2 nodes at level 1, so the loop should run twice, right?
Not quite. Every time you process a node inside that loop, you enqueue its children. The queue grows while the loop is checking its length. By the time i reaches 2, queue.length might be 5. The loop bound is a moving target -- it keeps rising as you add children, so you never exit the inner loop at the right moment. Level 1 nodes and level 2 nodes get mixed into the same batch.
Step through the animation below and watch the boundary dissolve. Notice the exact moment a child node gets processed in the same iteration as its parent.
// Buggy: no size snapshot!for (let i = 0; i < queue.length; i++) { // queue.length changes as children enqueue!}The fix is exactly one line: const size = queue.length, captured before the inner loop begins.
Why does this work? At the start of each outer iteration, every node currently in the queue belongs to the same level. Freezing queue.length into a local variable gives you a snapshot of how many same-level nodes exist right now. The inner loop runs exactly size times, processing only those nodes. Any children enqueued during the loop land at the back of the queue -- they belong to the next level and will be counted in the next outer iteration's snapshot.
Notice the elegance: you never need a delimiter, a sentinel value, or a second queue. One integer is enough to carve levels out of a flat FIFO stream. The BFS algorithm itself is unchanged. You are just freezing a number that would otherwise shift under you.
Step through the same tree with the snapshot in place and compare. Watch how the level boundaries stay crisp.
function levelOrder(root) { const result = [] const queue = [root] while (queue.length > 0) { const size = queue.length const level = [] for (let i = 0; i < size; i++) { const node = queue.shift() level.push(node.val) if (node.left) queue.push(node.left) if (node.right) queue.push(node.right) } result.push(level) } return result}Time to internalize the pattern. You have a fresh tree and no queue to help you -- just your understanding of BFS level boundaries.
This exercise is harder than it looks, because the tree structure itself can be misleading. On a balanced binary tree, the levels are visually obvious -- each row of the drawing corresponds to a BFS level. But on an unbalanced tree (one branch much deeper than another), your eyes want to group nodes by visual position, not by distance from the root. A deep left child and a shallow right grandchild might be at the same BFS level even though they appear at different heights in the drawing.
The mental model to use: start at the root (level 0). Everything one edge away is level 1. Everything two edges away is level 2. The levels are defined by edge distance, not by visual position. If you can trace the edges and count hops, you can predict the levels correctly even on trees that look unbalanced or irregular.
For each level, tap the nodes you think belong to it, then check your answer. The tree lights up as you go. If you picked correctly, it means you have the snapshot model in your head: you know exactly which nodes were “in the queue” at the start of each level, and which are children waiting for the next round.
Here is a level-order implementation that looks almost right. It compiles, it runs, and it even produces output that resembles grouped levels. But one line is wrong -- the snapshot bug is hiding in plain sight.
The dangerous thing about this bug is that it does not crash. It silently merges adjacent levels, giving you arrays that are too long and too few. On a balanced binary tree, you might get 3 “levels” instead of 4. On a skewed tree, you might get a single flat array. The output looks like levels but the grouping is wrong. If you tested this code on a simple 3-node tree (root with two children), it might even produce the correct answer by coincidence -- the inner loop would happen to process the right number of nodes. The bug only reveals itself on trees with enough depth for the level bleed to become visible.
This is the class of bug that survives unit testing and fails in production. A test with a trivial tree passes. A test with a moderate tree might also pass if you only check that the output contains the right nodes (it does -- all nodes are visited). Only a test that checks the grouping -- the exact partition into levels -- will catch it. And most developers do not write that test.
Find the line. Tap it.
Once you own the snapshot pattern, variants become trivial. Zigzag level-order traversal (LC 103) alternates direction each level: left-to-right, then right-to-left, then left-to-right again.
This is one of those problems that looks harder than it is, and the reason is that people try to modify the BFS traversal itself -- reversing the order of enqueuing children, using a deque that alternates push direction, or swapping between two stacks. All of those approaches technically work, but they add complexity to the traversal, which is the part you want to keep simple and invariant. The snapshot pattern gives you a cleaner decomposition: keep the traversal standard (FIFO, left-to-right), and handle the zigzag entirely in the output step.
The BFS itself does not change direction. You still dequeue and enqueue in the same order. The trick is what you do after collecting a level's nodes: on odd-numbered levels, you reverse the array before pushing it to the result. The snapshot gives you clean level boundaries; the reversal gives you the zigzag. This separation of concerns -- traversal order versus output order -- is the key insight.
Predict the correct zigzag order for each level below. Notice how it is the output order that alternates, not the traversal order.
You have seen the bug, the fix, and the variant. Now construct the template yourself.
This template is deceptively simple -- three pieces of code, maybe ten lines total. But those ten lines solve a remarkable number of tree problems. Binary Tree Level Order Traversal (LC 102), Binary Tree Right Side View (LC 199), Average of Levels (LC 637), Largest Value in Each Tree Row (LC 515), Zigzag Level Order (LC 103) -- every one of these is the same skeleton with a different one-line operation inside the inner loop. The skeleton is the investment; the variants are free.
The reason construction matters here, and not just recognition, is that the snapshot line is easy to forget under pressure. You remember “BFS with a queue” and write a clean while loop. It runs, it visits every node, the output looks plausible. But without the snapshot, the level boundaries blur, and your grouped output is silently wrong. The bug does not crash -- it just produces arrays of the wrong lengths. You will not catch it unless you test with a specific tree and manually verify the grouping.
Three blanks span the entire pattern: initializing the queue, snapshotting the level size, and collecting the current level before moving to the next. Every blank targets a different conceptual piece -- queue setup, the frozen boundary, and the level accumulation. If you can fill all three without hesitation, level-order BFS is yours.
Three questions to lock in the snapshot pattern.
The snapshot feels like a tiny detail -- one line of code, one local variable. But it is the hinge that separates “BFS traversal” from “level-order BFS.” Without it, you have a flat stream of nodes in BFS order. With it, you have nodes grouped by distance from the root, which is what every level-order problem actually asks for. The difference between those two outputs is the difference between a correct solution and one that silently merges adjacent levels.
The first question tests whether you understand what the snapshot captures and why it matters -- not mechanically, but in terms of the invariant it preserves (all nodes currently in the queue belong to at most two adjacent levels). The second tests when you need it: not every BFS needs level grouping, and adding the snapshot to a plain shortest-path BFS is unnecessary overhead. The third applies the pattern to a real problem: Binary Tree Right Side View (LC 199), where you need the last node of each level. That problem is a one-line change inside the snapshot loop -- but only if you have the snapshot in place.
If you can answer all three, you own this pattern cold.
What does const size = queue.length do before the inner loop?