Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Breadth-First Search (BFS)
P0EasyMental model~6 min speedrun

Level Order Traversal (BFS by Level)

Grows into
Topological SortMulti-Source BFSGraph BFS
Unlocks
Shortest-Path BFS
The problem

Binary Tree Level Order Traversal (LC 102)(opens leetcode.com)

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.

Example 1
Input:root = [3,9,20,null,null,15,7]
Output:[[3],[9,20],[15,7]]
Depth 0 holds 3, depth 1 holds 9 and 20, depth 2 holds 15 and 7.
9315207

Level-Order BFS

root ➔ freeze level_size = len(queue) ➔ batch process ring
Spot it when: The output is grouped by depth: one list per level, zigzag order, the rightmost node or the sum of each level.
Level-Order BFS
Python
1from collections import deque
2from typing import Callable, Iterable, List, Optional, TypeVar
3 
4Item = TypeVar("Item")
5 
6class 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 
13def 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)
Invariant: At the start of each round, 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 0 · len(queue) 1 · current_level [] · levels []

level_size 1 · len(queue) 1 · current_level [] · levels []

level_size 1 · len(queue) 0 · current_level [3] · levels []

level_size 1 · len(queue) 2 · current_level [3] · levels []

level_size 1 · len(queue) 2 · current_level [3] · levels []

level_size 1 · len(queue) 2 · current_level [3] · levels [[3]]

level_size 2 · len(queue) 2 · current_level [] · levels [[3]]

level_size 2 · len(queue) 1 · current_level [9] · levels [[3]]

level_size 2 · len(queue) 0 · current_level [9, 20] · levels [[3]]

level_size 2 · len(queue) 2 · current_level [9, 20] · levels [[3]]

level_size 2 · len(queue) 2 · current_level [9, 20] · levels [[3], [9, 20]]

level_size 2 · len(queue) 2 · current_level [] · levels [[3], [9, 20]]

level_size 2 · len(queue) 1 · current_level [15] · levels [[3], [9, 20]]

level_size 2 · len(queue) 0 · current_level [15, 7] · levels [[3], [9, 20]]

level_size 2 · len(queue) 0 · current_level [15, 7] · levels [[3], [9, 20], [15, 7]]

level_size 2 · len(queue) 0 · answer [[3], [9, 20], [15, 7]]

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.

  • node the node popped now
  • in level a node of this level that is waiting
  • queued a node just added to the queue
  • level a node of the level that was just recorded

queue = deque([root]) holds the root 3. It is level 0.

level_size = len(queue) = 1. Level 0 is the 1 node in queue now.

popleft() returns 3, and 3 is added to current_level.

3 adds 9 and 20 to the back of queue. len(queue) is now 2.

len(queue) is 2, but level_size is still 1. A while queue loop would pop 9 in this round.

levels.append(current_level) records [3] as level 0.

level_size = len(queue) = 2. Level 1 is the 2 nodes in queue now.

popleft() returns 9, and 9 is added to current_level. 9 has no children.

popleft() returns 20, and 20 is added to current_level.

20 adds 15 and 7 to the back of queue. len(queue) is now 2.

levels.append(current_level) records [9, 20] as level 1.

level_size = len(queue) = 2. Level 2 is the 2 nodes in queue now.

popleft() returns 15, and 15 is added to current_level. 15 has no children.

popleft() returns 7, and 7 is added to current_level. 7 has no children.

levels.append(current_level) records [15, 7] as level 2.

queue is empty, so the loop ends. levelOrder returns [[3], [9, 20], [15, 7]].

queue
13queued
2
3
4
5
popped
1 / 16
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Looping while queue inside the round

    Mistake
    while queue:
    Fix
    for _ in range(level_size):

    What happens: Children are added to the back of queue during the round. while queue keeps 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]]
  2. 2

    Gotcha 2: Reading queue[-1] after the round instead of before it

    Mistake
    for _ in range(level_size):
        ...
    view.append(queue[-1].val)
    Fix
    view.append(queue[-1].val)
    for _ in range(level_size):
        ...

    What happens: Binary Tree Right Side View (LC 199) keeps the last node of each level. Before the round, queue[-1] is that node. After the round, the queue holds the next level, so queue[-1] is the next level's last node. After the last level the queue is empty, and queue[-1] raises an IndexError.

    Example: root = [1,2,3,null,5,null,4]
    Mistake: raises IndexErrorFix: returns [1, 3, 4]
  3. 3

    Gotcha 3: Running it on a graph without a seen set

    Mistake
    queue.append(nxt)
    Fix
    if nxt not in seen:
        seen.add(nxt)
        queue.append(nxt)

    What happens: In a tree, each node has one parent, so it is added once. In a graph, two nodes can both point to the same node. Without seen, that node is added once per edge into it. In the example, node 3 appears twice in its level. Mark each node when you add it, as in Shortest-Path BFS.

    Example: adj = {0: [1, 2], 1: [3], 2: [3], 3: []}, start = 0
    Mistake: returns [[0], [1, 2], [3, 3]]Fix: returns [[0], [1, 2], [3]]
  4. 4

    Gotcha 4: Using list.pop(0) instead of deque.popleft()

    Mistake
    node = queue.pop(0)
    Fix
    node = queue.popleft()

    What happens: list.pop(0) moves every other item one place to the left, so it costs O(n) time. BFS pops every node once, so the whole walk costs O(n^2) time. collections.deque removes from the front in O(1) time. The answer is the same, only slower.

Not this one if…
  • 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_size at all? Why not store a depth with each node?

You can store (node, depth) pairs, and it works. But level_size gives 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 for queue and current_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. The levels result 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_level on every second level. Use a boolean that flips each round, or the parity of len(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 when depth == len(levels), append a new list. Then add node.val to levels[depth]. It has the same O(N) time. Its space is the recursion depth, which is O(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 seen set that holds the start nodes, and add a neighbour to queue only if it is not in seen. 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 seen set as well.

The invariant, step by step

  1. queue starts with the root. Before the loop, it holds exactly level 0.
  2. A round begins with level_size = len(queue). At this moment queue holds one whole level and nothing else, so level_size is the size of that level.
  3. The inner loop pops exactly level_size nodes from the front. Each popped node is added to current_level.
  4. 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.
  5. When the inner loop ends, all nodes of the level were popped and none of the next level was touched. current_level is the full level, so levels.append(current_level) records it.
  6. Now queue holds exactly the next level. The invariant is true again, so the next round starts correctly. The loop ends when a level adds no children and queue is empty.

Traps

  • while queue inside 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. Use for _ in range(level_size).
  • level_size read 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 an IndexError after the last level. Read it before the inner loop.
  • No seen set 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 costs O(N), so the walk costs O(N^2). Use deque.popleft().
  • Empty tree. deque([None]) would put None in the queue. Return [] first when root is empty.

Practice

Canonical LeetCode:Binary Tree Level Order Traversal (LC 102)Binary Tree Zigzag Level Order Traversal (LC 103)Deepest Leaves Sum (LC 1302)Binary Tree Right Side View (LC 199)Average of Levels in Binary Tree (LC 637)Find Largest Value in Each Tree Row (LC 515)Maximum Level Sum of a Binary Tree (LC 1161)
Theory Context•Breadth-First Search (BFS)
P0EasyMental model~6 min speedrun

Level Order Traversal (BFS by Level)

Grows into
Topological SortMulti-Source BFSGraph BFS
Unlocks
Shortest-Path BFS
The problem

Binary Tree Level Order Traversal (LC 102)(opens leetcode.com)

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.

Example 1
Input:root = [3,9,20,null,null,15,7]
Output:[[3],[9,20],[15,7]]
Depth 0 holds 3, depth 1 holds 9 and 20, depth 2 holds 15 and 7.
9315207

Level-Order BFS

root ➔ freeze level_size = len(queue) ➔ batch process ring
Spot it when: The output is grouped by depth: one list per level, zigzag order, the rightmost node or the sum of each level.
Level-Order BFS
Python
1from collections import deque
2from typing import Callable, Iterable, List, Optional, TypeVar
3 
4Item = TypeVar("Item")
5 
6class 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 
13def 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)
Invariant: At the start of each round, 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 0 · len(queue) 1 · current_level [] · levels []

level_size 1 · len(queue) 1 · current_level [] · levels []

level_size 1 · len(queue) 0 · current_level [3] · levels []

level_size 1 · len(queue) 2 · current_level [3] · levels []

level_size 1 · len(queue) 2 · current_level [3] · levels []

level_size 1 · len(queue) 2 · current_level [3] · levels [[3]]

level_size 2 · len(queue) 2 · current_level [] · levels [[3]]

level_size 2 · len(queue) 1 · current_level [9] · levels [[3]]

level_size 2 · len(queue) 0 · current_level [9, 20] · levels [[3]]

level_size 2 · len(queue) 2 · current_level [9, 20] · levels [[3]]

level_size 2 · len(queue) 2 · current_level [9, 20] · levels [[3], [9, 20]]

level_size 2 · len(queue) 2 · current_level [] · levels [[3], [9, 20]]

level_size 2 · len(queue) 1 · current_level [15] · levels [[3], [9, 20]]

level_size 2 · len(queue) 0 · current_level [15, 7] · levels [[3], [9, 20]]

level_size 2 · len(queue) 0 · current_level [15, 7] · levels [[3], [9, 20], [15, 7]]

level_size 2 · len(queue) 0 · answer [[3], [9, 20], [15, 7]]

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.

  • node the node popped now
  • in level a node of this level that is waiting
  • queued a node just added to the queue
  • level a node of the level that was just recorded

queue = deque([root]) holds the root 3. It is level 0.

level_size = len(queue) = 1. Level 0 is the 1 node in queue now.

popleft() returns 3, and 3 is added to current_level.

3 adds 9 and 20 to the back of queue. len(queue) is now 2.

len(queue) is 2, but level_size is still 1. A while queue loop would pop 9 in this round.

levels.append(current_level) records [3] as level 0.

level_size = len(queue) = 2. Level 1 is the 2 nodes in queue now.

popleft() returns 9, and 9 is added to current_level. 9 has no children.

popleft() returns 20, and 20 is added to current_level.

20 adds 15 and 7 to the back of queue. len(queue) is now 2.

levels.append(current_level) records [9, 20] as level 1.

level_size = len(queue) = 2. Level 2 is the 2 nodes in queue now.

popleft() returns 15, and 15 is added to current_level. 15 has no children.

popleft() returns 7, and 7 is added to current_level. 7 has no children.

levels.append(current_level) records [15, 7] as level 2.

queue is empty, so the loop ends. levelOrder returns [[3], [9, 20], [15, 7]].

queue
13queued
2
3
4
5
popped
1 / 16
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Looping while queue inside the round

    Mistake
    while queue:
    Fix
    for _ in range(level_size):

    What happens: Children are added to the back of queue during the round. while queue keeps 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]]
  2. 2

    Gotcha 2: Reading queue[-1] after the round instead of before it

    Mistake
    for _ in range(level_size):
        ...
    view.append(queue[-1].val)
    Fix
    view.append(queue[-1].val)
    for _ in range(level_size):
        ...

    What happens: Binary Tree Right Side View (LC 199) keeps the last node of each level. Before the round, queue[-1] is that node. After the round, the queue holds the next level, so queue[-1] is the next level's last node. After the last level the queue is empty, and queue[-1] raises an IndexError.

    Example: root = [1,2,3,null,5,null,4]
    Mistake: raises IndexErrorFix: returns [1, 3, 4]
  3. 3

    Gotcha 3: Running it on a graph without a seen set

    Mistake
    queue.append(nxt)
    Fix
    if nxt not in seen:
        seen.add(nxt)
        queue.append(nxt)

    What happens: In a tree, each node has one parent, so it is added once. In a graph, two nodes can both point to the same node. Without seen, that node is added once per edge into it. In the example, node 3 appears twice in its level. Mark each node when you add it, as in Shortest-Path BFS.

    Example: adj = {0: [1, 2], 1: [3], 2: [3], 3: []}, start = 0
    Mistake: returns [[0], [1, 2], [3, 3]]Fix: returns [[0], [1, 2], [3]]
  4. 4

    Gotcha 4: Using list.pop(0) instead of deque.popleft()

    Mistake
    node = queue.pop(0)
    Fix
    node = queue.popleft()

    What happens: list.pop(0) moves every other item one place to the left, so it costs O(n) time. BFS pops every node once, so the whole walk costs O(n^2) time. collections.deque removes from the front in O(1) time. The answer is the same, only slower.

Not this one if…
  • 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_size at all? Why not store a depth with each node?

You can store (node, depth) pairs, and it works. But level_size gives 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 for queue and current_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. The levels result 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_level on every second level. Use a boolean that flips each round, or the parity of len(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 when depth == len(levels), append a new list. Then add node.val to levels[depth]. It has the same O(N) time. Its space is the recursion depth, which is O(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 seen set that holds the start nodes, and add a neighbour to queue only if it is not in seen. 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 seen set as well.

The invariant, step by step

  1. queue starts with the root. Before the loop, it holds exactly level 0.
  2. A round begins with level_size = len(queue). At this moment queue holds one whole level and nothing else, so level_size is the size of that level.
  3. The inner loop pops exactly level_size nodes from the front. Each popped node is added to current_level.
  4. 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.
  5. When the inner loop ends, all nodes of the level were popped and none of the next level was touched. current_level is the full level, so levels.append(current_level) records it.
  6. Now queue holds exactly the next level. The invariant is true again, so the next round starts correctly. The loop ends when a level adds no children and queue is empty.

Traps

  • while queue inside 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. Use for _ in range(level_size).
  • level_size read 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 an IndexError after the last level. Read it before the inner loop.
  • No seen set 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 costs O(N), so the walk costs O(N^2). Use deque.popleft().
  • Empty tree. deque([None]) would put None in the queue. Return [] first when root is empty.

Practice

Canonical LeetCode:Binary Tree Level Order Traversal (LC 102)Binary Tree Zigzag Level Order Traversal (LC 103)Deepest Leaves Sum (LC 1302)Binary Tree Right Side View (LC 199)Average of Levels in Binary Tree (LC 637)Find Largest Value in Each Tree Row (LC 515)Maximum Level Sum of a Binary Tree (LC 1161)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•