Level Order Traversal (BFS by Level)
You have the root of a binary tree. Walk it breadth-first, visiting each depth in left-to-right order, and produce one list of values per depth — so the result is a list of lists, ordered from the root's level down to the leaves' level.
3, depth 1 holds 9 and 20, depth 2 holds 15 and 7.Level-Order BFS
root ➔ freeze level_size = len(queue) ➔ batch process ring| 1 | from collections import deque |
| 2 | from typing import Callable, Iterable, List, Optional, TypeVar |
| 3 | |
| 4 | Item = TypeVar("Item") |
| 5 | |
| 6 | class TreeNode: |
| 7 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 8 | self.val = val |
| 9 | self.left = left |
| 10 | self.right = right |
| 11 | |
| 12 | |
| 13 | def level_by_level(start: Iterable[Item], children: Callable[[Item], Iterable[Item]], record: Callable[[List[Item]], None]) -> None: |
| 14 | """Level-Order BFS: hand the nodes of each level, left to right, to record(level), level after level. |
| 15 | start is the first level. children(node) lists the nodes of the next level below node. |
| 16 | Precondition: every node is reached once, as in a tree, or children skips the nodes already seen. |
| 17 | V = the nodes. W = the largest level. T = the cost of one children call, or of record for one node. |
| 18 | Time: O(V * T) | Space: O(W) queue and current level |
| 19 | """ |
| 20 | queue = deque(start) # holds nodes queued for this or a later level, in enqueue order |
| 21 | |
| 22 | while queue: |
| 23 | # 1. Snapshot the Level Boundary: |
| 24 | # len(queue) right now counts only nodes queued from the previous level, so |
| 25 | # freezing it before the inner loop keeps this level's own children out of |
| 26 | # the current sweep. |
| 27 | level_size = len(queue) |
| 28 | current_level = [] |
| 29 | |
| 30 | for _ in range(level_size): |
| 31 | # 2. Pop and Record This Level's Node: |
| 32 | # popleft() returns nodes in the order they were queued, so the |
| 33 | # earliest-queued node is always processed first. |
| 34 | node = queue.popleft() |
| 35 | current_level.append(node) |
| 36 | queue.extend(children(node)) |
| 37 | |
| 38 | # 3. Record the Completed Level: |
| 39 | record(current_level) |
queue holds exactly the nodes of one level, left to right, and level_size is their count.Read len(queue) into level_size at the start of each round: exactly that many nodes belong to the current level. Process only those, adding their children, and the queue then holds just the next level. That gives level boundaries without storing a depth per node. The trap is re-reading the size while children arrive: the queue grows during the round, so a bound that checks len(queue) again (a while loop, or a loop in another language) runs on into the next level; freeze it in level_size first.
See it run
Level-Order BFS on root = [3, 9, 20, null, null, 15, 7], step by step
level_size 0len(queue) 1current_level []levels []
Each column is one node, in the order it is added to queue. The top row is queue. The bottom row shows a node's value once it is popped.
nodethe node popped nowin levela node of this level that is waitingqueueda node just added to the queuelevela node of the level that was just recorded
queue = deque([root]) holds the root 3. It is level 0.
Gotcha 1 of 4
- The trap
Gotcha 1: Looping
while queueinside the roundMistakewhile queue:Fixfor _ in range(level_size):What happens: Children are added to the back of
queueduring the round.while queuekeeps popping them in the same round. So the loop runs on into the next level, and every node ends up in one list.level_size = len(queue)is read once, before any child is added. The inner loop runs exactly that many times.Example: Binary Tree Level Order Traversal (LC 102): root = [3,9,20,null,null,15,7]Mistake: returns [[3, 9, 20, 15, 7]]Fix: returns [[3], [9, 20], [15, 7]]
- You need the length of the shortest route to one target cell, as in Shortest Path in Binary Matrix (LC 1091), not the nodes grouped by level. See Shortest-Path BFS
Deep Dive
Why level_size marks the level boundary
A queue gives first in, first out order. In a tree, every node of depth d is added to queue before any node of depth d + 1, because depth d + 1 nodes are added only while depth d nodes are being popped. So the queue always holds the nodes of one depth, then the nodes of the next depth, with no mixing.
At the start of a round, queue holds exactly one level. level_size = len(queue) is the number of nodes in that level. The inner loop pops exactly level_size nodes. The children they add go behind them, so they cannot be reached in this round.
In Binary Tree Level Order Traversal (LC 102) with root = [3, 9, 20, null, null, 15, 7], the rounds read level_size = 1, 2 and 2. They produce [[3], [9, 20], [15, 7]]. No depth is stored per node. The size of the queue at the start of the round is enough.
The trap: the queue grows during the round
queue.extend and queue.append run inside the round, so len(queue) changes while the loop runs. After 3 is popped and its children are added, len(queue) is 2, but level_size is still 1. The round must stop here.
If the loop is while queue: instead of for _ in range(level_size):, the round does not stop. It pops 9 and 20 in the same round, then 15 and 7. For root = [3, 9, 20, null, null, 15, 7], the result is [[3, 9, 20, 15, 7]] instead of [[3], [9, 20], [15, 7]].
In Python, range(level_size) is built once, so it cannot change. In a language where the loop condition is checked again each time, writing i < queue.size() has the same fault as while queue. Always copy the size into level_size first.
Where to read the level: before, during or after the round
Each problem on the card asks for one value per level. Where you read it decides the answer.
Binary Tree Right Side View (LC 199) needs the last node of each level. Read queue[-1] before the round, when queue still holds only this level. After the round, queue holds the next level, and after the last level it is empty, so queue[-1] raises IndexError. For root = [1, 2, 3, null, 5, null, 4], reading before gives [1, 3, 4].
Average of Levels in Binary Tree (LC 637), Find Largest Value in Each Tree Row (LC 515) and Maximum Level Sum of a Binary Tree (LC 1161) read each popped node.val inside the round and combine the values after it. Binary Tree Zigzag Level Order Traversal (LC 103) reverses current_level on every second level, either with a flag or with level_index % 2.
Deepest Leaves Sum (LC 1302) keeps only the last current_level, because the last level that is recorded is the deepest one.
The cost, and what changes on a graph
Every node is added once and popped once. Each pop does constant work in levelOrder, so the time is O(V * T), which is O(N) for a tree with N nodes. The space is O(W). W is the largest number of nodes in one level, because queue and current_level hold at most one level and its children.
W depends on the shape. A tree where every node has one child has W = 1. A complete binary tree has about N / 2 nodes in its last level, so W is about N / 2.
A tree has no cycles, so no node is reached twice. A graph can reach one node from two parents. Then add a seen set and mark a node when it is added, not when it is popped. Without it, the graph {0: [1, 2], 1: [3], 2: [3], 3: []} puts node 3 in its level twice. Shortest-Path BFS is the card that uses this rule.
Use deque.popleft() and not list.pop(0). list.pop(0) moves every other item, so a walk over N nodes costs O(N^2) time.
60-Second Pitch
"The output is grouped by level, so I use a queue and process one level per round. At the start of each round, queue holds exactly one level, so I copy len(queue) into level_size. I pop exactly that many nodes, record their values, and add their children to the back. Those children are the next level. I must freeze the size before the loop, because the queue grows while the loop runs. A condition that checks the queue again would run into the next level. Each node is added once and popped once, so the time is O(N). The space is O(W), where W is the widest level. For a graph, I would add a seen set and mark each node when it is added."
Common Questions
- Why do you need
level_sizeat all? Why not store a depth with each node? You can store
(node, depth)pairs, and it works. Butlevel_sizegives the same boundary with no extra value per node. The queue itself separates the levels, so one integer per round is enough.- What are the time and space complexity of
levelOrder? O(N)time, because each node is added once and popped once with constant work. O(W) space forqueueandcurrent_level, where W is the number of nodes in the widest level. W is 1 for a chain and about N / 2 for a complete binary tree. Thelevelsresult itself holds N values.- How do you change it for Binary Tree Zigzag Level Order Traversal (LC 103)?
Keep the traversal the same, and reverse
current_levelon every second level. Use a boolean that flips each round, or the parity oflen(levels). The queue order stays left to right, because the children must still be added left first.- Could you solve it with depth-first search instead?
Yes. Call
dfs(node, depth), and whendepth == len(levels), append a new list. Then addnode.valtolevels[depth]. It has the sameO(N)time. Its space is the recursion depth, which isO(N)for a chain, so a very deep tree can exceed Python's recursion limit. BFS has no recursion.- What changes when the input is a graph and not a tree?
Add a
seenset that holds the start nodes, and add a neighbour toqueueonly if it is not inseen. Mark it when you add it, not when you pop it. Then each node is added once, and the levels are the distances from the start. That is the idea of Shortest-Path BFS.
SWE Reasoning
Spot it
- The answer is grouped by depth: one list per level, one value per level, or the nodes on one side of the tree.
- The wording names levels, rows, layers, depth, "left to right" or "zigzag".
- A depth-first search would need a depth argument and a list of lists indexed by depth. Level order BFS avoids that bookkeeping.
- Each level can be processed with a pass over the nodes of that level, such as a sum, a maximum, a last element or a reversal.
- The input is a tree, so there is no cycle. If it is a graph, you need a
seenset as well.
The invariant, step by step
queuestarts with the root. Before the loop, it holds exactly level 0.- A round begins with
level_size = len(queue). At this momentqueueholds one whole level and nothing else, solevel_sizeis the size of that level. - The inner loop pops exactly
level_sizenodes from the front. Each popped node is added tocurrent_level. - Each popped node adds its existing children to the back. They belong to the next level, and they stay behind the nodes that are still waiting in this level.
- When the inner loop ends, all nodes of the level were popped and none of the next level was touched.
current_levelis the full level, solevels.append(current_level)records it. - Now
queueholds exactly the next level. The invariant is true again, so the next round starts correctly. The loop ends when a level adds no children andqueueis empty.
Traps
while queueinside the round. The queue grows during the round, so the loop does not stop at the level boundary. Every node ends up in one list. Usefor _ in range(level_size).level_sizeread after children are added. The size then includes some of the next level. Read it once at the start of the round.queue[-1]read after the round. It is the last node of the next level, or anIndexErrorafter the last level. Read it before the inner loop.- No
seenset on a graph. A node with two parents is added twice and appears twice in its level. Mark each node when you add it. list.pop(0). The answer is correct, but each pop costsO(N), so the walk costs O(N^2). Usedeque.popleft().- Empty tree.
deque([None])would putNonein the queue. Return[]first whenrootis empty.
Practice
Level Order Traversal (BFS by Level)
You have the root of a binary tree. Walk it breadth-first, visiting each depth in left-to-right order, and produce one list of values per depth — so the result is a list of lists, ordered from the root's level down to the leaves' level.
3, depth 1 holds 9 and 20, depth 2 holds 15 and 7.Level-Order BFS
root ➔ freeze level_size = len(queue) ➔ batch process ring| 1 | from collections import deque |
| 2 | from typing import Callable, Iterable, List, Optional, TypeVar |
| 3 | |
| 4 | Item = TypeVar("Item") |
| 5 | |
| 6 | class TreeNode: |
| 7 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 8 | self.val = val |
| 9 | self.left = left |
| 10 | self.right = right |
| 11 | |
| 12 | |
| 13 | def level_by_level(start: Iterable[Item], children: Callable[[Item], Iterable[Item]], record: Callable[[List[Item]], None]) -> None: |
| 14 | """Level-Order BFS: hand the nodes of each level, left to right, to record(level), level after level. |
| 15 | start is the first level. children(node) lists the nodes of the next level below node. |
| 16 | Precondition: every node is reached once, as in a tree, or children skips the nodes already seen. |
| 17 | V = the nodes. W = the largest level. T = the cost of one children call, or of record for one node. |
| 18 | Time: O(V * T) | Space: O(W) queue and current level |
| 19 | """ |
| 20 | queue = deque(start) # holds nodes queued for this or a later level, in enqueue order |
| 21 | |
| 22 | while queue: |
| 23 | # 1. Snapshot the Level Boundary: |
| 24 | # len(queue) right now counts only nodes queued from the previous level, so |
| 25 | # freezing it before the inner loop keeps this level's own children out of |
| 26 | # the current sweep. |
| 27 | level_size = len(queue) |
| 28 | current_level = [] |
| 29 | |
| 30 | for _ in range(level_size): |
| 31 | # 2. Pop and Record This Level's Node: |
| 32 | # popleft() returns nodes in the order they were queued, so the |
| 33 | # earliest-queued node is always processed first. |
| 34 | node = queue.popleft() |
| 35 | current_level.append(node) |
| 36 | queue.extend(children(node)) |
| 37 | |
| 38 | # 3. Record the Completed Level: |
| 39 | record(current_level) |
queue holds exactly the nodes of one level, left to right, and level_size is their count.Read len(queue) into level_size at the start of each round: exactly that many nodes belong to the current level. Process only those, adding their children, and the queue then holds just the next level. That gives level boundaries without storing a depth per node. The trap is re-reading the size while children arrive: the queue grows during the round, so a bound that checks len(queue) again (a while loop, or a loop in another language) runs on into the next level; freeze it in level_size first.
See it run
Level-Order BFS on root = [3, 9, 20, null, null, 15, 7], step by step
level_size 0len(queue) 1current_level []levels []
Each column is one node, in the order it is added to queue. The top row is queue. The bottom row shows a node's value once it is popped.
nodethe node popped nowin levela node of this level that is waitingqueueda node just added to the queuelevela node of the level that was just recorded
queue = deque([root]) holds the root 3. It is level 0.
Gotcha 1 of 4
- The trap
Gotcha 1: Looping
while queueinside the roundMistakewhile queue:Fixfor _ in range(level_size):What happens: Children are added to the back of
queueduring the round.while queuekeeps popping them in the same round. So the loop runs on into the next level, and every node ends up in one list.level_size = len(queue)is read once, before any child is added. The inner loop runs exactly that many times.Example: Binary Tree Level Order Traversal (LC 102): root = [3,9,20,null,null,15,7]Mistake: returns [[3, 9, 20, 15, 7]]Fix: returns [[3], [9, 20], [15, 7]]
- You need the length of the shortest route to one target cell, as in Shortest Path in Binary Matrix (LC 1091), not the nodes grouped by level. See Shortest-Path BFS
Deep Dive
Why level_size marks the level boundary
A queue gives first in, first out order. In a tree, every node of depth d is added to queue before any node of depth d + 1, because depth d + 1 nodes are added only while depth d nodes are being popped. So the queue always holds the nodes of one depth, then the nodes of the next depth, with no mixing.
At the start of a round, queue holds exactly one level. level_size = len(queue) is the number of nodes in that level. The inner loop pops exactly level_size nodes. The children they add go behind them, so they cannot be reached in this round.
In Binary Tree Level Order Traversal (LC 102) with root = [3, 9, 20, null, null, 15, 7], the rounds read level_size = 1, 2 and 2. They produce [[3], [9, 20], [15, 7]]. No depth is stored per node. The size of the queue at the start of the round is enough.
The trap: the queue grows during the round
queue.extend and queue.append run inside the round, so len(queue) changes while the loop runs. After 3 is popped and its children are added, len(queue) is 2, but level_size is still 1. The round must stop here.
If the loop is while queue: instead of for _ in range(level_size):, the round does not stop. It pops 9 and 20 in the same round, then 15 and 7. For root = [3, 9, 20, null, null, 15, 7], the result is [[3, 9, 20, 15, 7]] instead of [[3], [9, 20], [15, 7]].
In Python, range(level_size) is built once, so it cannot change. In a language where the loop condition is checked again each time, writing i < queue.size() has the same fault as while queue. Always copy the size into level_size first.
Where to read the level: before, during or after the round
Each problem on the card asks for one value per level. Where you read it decides the answer.
Binary Tree Right Side View (LC 199) needs the last node of each level. Read queue[-1] before the round, when queue still holds only this level. After the round, queue holds the next level, and after the last level it is empty, so queue[-1] raises IndexError. For root = [1, 2, 3, null, 5, null, 4], reading before gives [1, 3, 4].
Average of Levels in Binary Tree (LC 637), Find Largest Value in Each Tree Row (LC 515) and Maximum Level Sum of a Binary Tree (LC 1161) read each popped node.val inside the round and combine the values after it. Binary Tree Zigzag Level Order Traversal (LC 103) reverses current_level on every second level, either with a flag or with level_index % 2.
Deepest Leaves Sum (LC 1302) keeps only the last current_level, because the last level that is recorded is the deepest one.
The cost, and what changes on a graph
Every node is added once and popped once. Each pop does constant work in levelOrder, so the time is O(V * T), which is O(N) for a tree with N nodes. The space is O(W). W is the largest number of nodes in one level, because queue and current_level hold at most one level and its children.
W depends on the shape. A tree where every node has one child has W = 1. A complete binary tree has about N / 2 nodes in its last level, so W is about N / 2.
A tree has no cycles, so no node is reached twice. A graph can reach one node from two parents. Then add a seen set and mark a node when it is added, not when it is popped. Without it, the graph {0: [1, 2], 1: [3], 2: [3], 3: []} puts node 3 in its level twice. Shortest-Path BFS is the card that uses this rule.
Use deque.popleft() and not list.pop(0). list.pop(0) moves every other item, so a walk over N nodes costs O(N^2) time.
60-Second Pitch
"The output is grouped by level, so I use a queue and process one level per round. At the start of each round, queue holds exactly one level, so I copy len(queue) into level_size. I pop exactly that many nodes, record their values, and add their children to the back. Those children are the next level. I must freeze the size before the loop, because the queue grows while the loop runs. A condition that checks the queue again would run into the next level. Each node is added once and popped once, so the time is O(N). The space is O(W), where W is the widest level. For a graph, I would add a seen set and mark each node when it is added."
Common Questions
- Why do you need
level_sizeat all? Why not store a depth with each node? You can store
(node, depth)pairs, and it works. Butlevel_sizegives the same boundary with no extra value per node. The queue itself separates the levels, so one integer per round is enough.- What are the time and space complexity of
levelOrder? O(N)time, because each node is added once and popped once with constant work. O(W) space forqueueandcurrent_level, where W is the number of nodes in the widest level. W is 1 for a chain and about N / 2 for a complete binary tree. Thelevelsresult itself holds N values.- How do you change it for Binary Tree Zigzag Level Order Traversal (LC 103)?
Keep the traversal the same, and reverse
current_levelon every second level. Use a boolean that flips each round, or the parity oflen(levels). The queue order stays left to right, because the children must still be added left first.- Could you solve it with depth-first search instead?
Yes. Call
dfs(node, depth), and whendepth == len(levels), append a new list. Then addnode.valtolevels[depth]. It has the sameO(N)time. Its space is the recursion depth, which isO(N)for a chain, so a very deep tree can exceed Python's recursion limit. BFS has no recursion.- What changes when the input is a graph and not a tree?
Add a
seenset that holds the start nodes, and add a neighbour toqueueonly if it is not inseen. Mark it when you add it, not when you pop it. Then each node is added once, and the levels are the distances from the start. That is the idea of Shortest-Path BFS.
SWE Reasoning
Spot it
- The answer is grouped by depth: one list per level, one value per level, or the nodes on one side of the tree.
- The wording names levels, rows, layers, depth, "left to right" or "zigzag".
- A depth-first search would need a depth argument and a list of lists indexed by depth. Level order BFS avoids that bookkeeping.
- Each level can be processed with a pass over the nodes of that level, such as a sum, a maximum, a last element or a reversal.
- The input is a tree, so there is no cycle. If it is a graph, you need a
seenset as well.
The invariant, step by step
queuestarts with the root. Before the loop, it holds exactly level 0.- A round begins with
level_size = len(queue). At this momentqueueholds one whole level and nothing else, solevel_sizeis the size of that level. - The inner loop pops exactly
level_sizenodes from the front. Each popped node is added tocurrent_level. - Each popped node adds its existing children to the back. They belong to the next level, and they stay behind the nodes that are still waiting in this level.
- When the inner loop ends, all nodes of the level were popped and none of the next level was touched.
current_levelis the full level, solevels.append(current_level)records it. - Now
queueholds exactly the next level. The invariant is true again, so the next round starts correctly. The loop ends when a level adds no children andqueueis empty.
Traps
while queueinside the round. The queue grows during the round, so the loop does not stop at the level boundary. Every node ends up in one list. Usefor _ in range(level_size).level_sizeread after children are added. The size then includes some of the next level. Read it once at the start of the round.queue[-1]read after the round. It is the last node of the next level, or anIndexErrorafter the last level. Read it before the inner loop.- No
seenset on a graph. A node with two parents is added twice and appears twice in its level. Mark each node when you add it. list.pop(0). The answer is correct, but each pop costsO(N), so the walk costs O(N^2). Usedeque.popleft().- Empty tree.
deque([None])would putNonein the queue. Return[]first whenrootis empty.