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>>whendepth == ans.size(), then recurse left and right. Correct. A spine isO(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
nullinto 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.