A catalog-pricing console stores a binary split tree: each node is a question (region, then plan, then tenure). The ops dashboard paints one horizontal strip per depth so a reviewer sees every question that fires at depth 0, then 1, then 2 — not a left-spine dump. The intern version kept two queues: this floor and the next floor, swapping when the current drained. A dozen splits painted instantly. A catalog with tens of thousands of nodes still returned, but the second queue was furniture a size snapshot already gives you.

Level Order asks for values grouped by depth, left to right. The binary tree post owns level-order as a layout walk. The BFS post owns why the frontier is a FIFO ArrayDeque. Here we only care about recording int size = q.size() and looping that many times so each inner pass is one floor. Do not paste a graph seen set or a shortest-path argument from that post.

The problem

Given the root of a binary tree, return its values grouped by depth: one list per floor, left to right inside the floor. An empty tree returns an empty list. A single node returns [[val]].

final class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int val) {
        this.val = val;
    }
}

Sample trees and the grouped return. Missing children do not leave holes in a level — only live nodes appear, still left to right.

      8
     / \
    3   10
       /  \
      9    14

→  [[8], [3, 10], [9, 14]]


      8
     /
    3
     \
      5

→  [[8], [3], [5]]


(root = null)  →  []
[1]            →  [[1]]

Note: Left-to-right is child order, not sorted order. Do not sort a level. A single node is [[val]], not [val].

Two queues, swap when the floor empties is the honest brute force

Keep current holding this depth. Poll it dry: collect each value, offer live children onto next. When current is empty, that collected list is one floor; swap next into current. Correct. Extra deque.

List<List<Integer>> levelOrderTwoQueues(TreeNode root) {
    List<List<Integer>> ans = new ArrayList<>();
    if (root == null) {
        return ans;
    }
    Deque<TreeNode> current = new ArrayDeque<>();
    current.offer(root);
    while (!current.isEmpty()) {
        Deque<TreeNode> next = new ArrayDeque<>();
        List<Integer> level = new ArrayList<>();
        while (!current.isEmpty()) {
            TreeNode node = current.poll();
            level.add(node.val);
            if (node.left != null) {
                next.offer(node.left);
            }
            if (node.right != null) {
                next.offer(node.right);
            }
        }
        ans.add(level);
        current = next;
    }
    return ans;
}

At a dozen nodes this is a rounding error. At tens of thousands you paid a second ArrayDeque for a question the same queue already answers if you snapshot its size: how many nodes are on this floor right now?

Snapshot size, drain that many

Offer the root. While the queue is not empty, record int size = q.size() — that many polls are this floor — then loop that count: poll, append the value, offer left then right when they exist. After the inner loop, append the floor list to the answer.

Snapshot size before you offer children. A single while (!q.isEmpty()) that offers as it polls flattens the tree into one list: the next floor lands on the same deque before you finish this one. The count is the grouping.

q: [8]
size=1  poll 8    offer 3, 10      level [8]
q: [3, 10]
size=2  poll 3    (no kids)        level [3]
        poll 10   offer 9, 14      level [3, 10]
q: [9, 14]
size=2  poll 9, poll 14            level [9, 14]
q: []

→ [[8], [3, 10], [9, 14]]

The Java is that walk. Use Deque and ArrayDeque, not java.util.Stack, not ArrayList.remove(0). Skip null children — ArrayDeque refuses null, and this prompt does not want holes.

List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> ans = new ArrayList<>();
    if (root == null) {
        return ans;
    }
    Deque<TreeNode> q = new ArrayDeque<>();
    q.offer(root);
    while (!q.isEmpty()) {
        int size = q.size();
        List<Integer> level = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            TreeNode node = q.poll();
            level.add(node.val);
            if (node.left != null) {
                q.offer(node.left);
            }
            if (node.right != null) {
                q.offer(node.right);
            }
        }
        ans.add(level);
    }
    return ans;
}

Time is O(n) — each node is offered once and polled once. Space is O(w) for the queue, w being the widest floor (plus the answer lists, which are the output). A perfect tree’s last floor is about n/2; a spine’s queue stays O(1). Do not quote a graph seen set; there is nothing to mark.

Note: Return [] for a null root before you offer. Offering null throws on ArrayDeque.

What interviewers usually poke next

  • DFS into buckets. Pass a depth index; grow List<List<Integer>> when depth == ans.size(), then recurse left and right. Correct. A spine is O(n) frames. They may ask this after the queue walk; it is not the default.
  • Right Side View. Same size snapshot; keep only the last poll of each floor. Different return. Future path /interview/trees/medium/binary-tree-right-side-view/.
  • Zigzag. Same drain; reverse every other floor, or poll from the opposite end. Future path /interview/trees/medium/binary-tree-zigzag-level-order-traversal/.
  • Serialize. Level-order that keeps nulls so the shape round-trips. Different prompt; do not start stuffing null into this return. Future path /interview/trees/hard/serialize-and-deserialize-binary-tree/.
  • Why no seen? A tree has no parent pointer back and at most two children. Mark-on-enqueue is the graph post’s cycle filter. Pasting it here is cargo.

You are done with this problem when you can snapshot q.size(), drain that many, walk [[8], [3, 10], [9, 14]] out loud, and say why a tree BFS does not need a seen set.