DSA sheet · Trees · BFS (level order) pattern
Zigzag Level Order Traversal
This is the problem right after plain level order traversal, and it builds directly on it. We still print the tree level by level, but the direction flips on every level: left to right, then right to left, then left to right again. The teacher first shows an easy brute force (do normal level order, then reverse every second level), then removes the extra reversing work with a deque (a list you can add to at both ends), and finally shows that the same answer is possible with DFS too. The real lesson: you can get a reversed list for free if you choose where to insert.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting
- Part A · Brute force: level order, then reverse the odd levels
- Part B · Optimal BFS: insert at the front or the back of a deque
- Part C · Zigzag with DFS (carry the level number)
- Part D · Revision page
Part 0 · Before starting
The tree node
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val # the number in this node
self.left = left # left child, or None
self.right = right # right child, or NoneWhat is a "level"?
A level is a row of the tree. The root alone is level 0. Its children are on level 1. Their children are on level 2, and so on. A simple rule: a child's level = its parent's level + 1. We count from 0, the same way list indexes start at 0.
3 ← level 0
/ \
9 20 ← level 1
/ \
15 7 ← level 2
Plain level order traversal (the prerequisite)
The teacher says this video only makes sense if you already know level order traversal from the previous video. Here it is again, because Parts A and B are small edits of this exact code.
from collections import deque
class Solution:
def levelOrder(self, root):
ans = []
if root is None:
return ans
queue = deque([root])
while queue:
size = len(queue) # how many nodes are on THIS level
level = []
for i in range(size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
ans.append(level) # one finished row
return ansFor the tree above it gives [[3], [9, 20], [15, 7]]. The key trick is size = len(queue): at the start of each round, the queue holds exactly one full level. So running the inner loop size times takes out that level and nothing more, even though we keep adding children at the back.
Part A · Brute force: level order, then reverse
LeetCode 103 · Binary Tree Zigzag Level Order Traversal
1The question in simple words
You get the root of a binary tree. Return the values level by level, as a list of lists, but in zigzag order:
- level 0: read left → right
- level 1: read right → left
- level 2: read left → right again, and keep alternating.
3 → [3]
/ \
9 20 ← [20, 9]
/ \
15 7 → [15, 7] 1 → [1]
/ \
2 3 ← [3, 2]
/ \ / \
4 5 6 7 → [4, 5, 6, 7]Answer for example 1: [[3], [20, 9], [15, 7]]. Notice we return the values of the nodes, not the node objects.
2What the constraints tell us
- Number of nodes: 0 to 2000 → 0 is allowed, so the root can be
None. In a BFS solution we must check this at the very start and return[], otherwise we would pushNoneinto the queue and crash on.val. - The teacher's general rule about base cases:
- BFS: the empty check is needed only when the tree can be empty. If the constraints promise at least one node, you can skip it, because the
while queueloop stops by itself. - DFS: always write the base case, whatever the constraints say. Recursion has no loop condition; the base case is the only thing that stops it.
- BFS: the empty check is needed only when the tree can be empty. If the constraints promise at least one node, you can skip it, because the
- Values: −100 to 100 → tiny numbers, a normal int is fine. We don't add or multiply them anyway.
- Why read constraints at all? They tell us (1) whether we need a base case, (2) whether int is big enough or we need long, and (3) whether a slow idea will pass. n ≤ 2000 is small, but we still aim for a clean O(n).
3Intuition: how to think about it
First decide BFS or DFS. The teacher's advice: whenever a question talks about levels, go with BFS first. BFS already walks the tree one level at a time, which is exactly what the output needs. DFS can also do it (Part C), but then we have to store and sort out levels ourselves.
Now picture the normal level order output and number the rows:
| level index | normal level order | zigzag wants | what to do |
|---|---|---|---|
| 0 (even) | [3] | [3] | same order, keep it |
| 1 (odd) | [9, 20] | [20, 9] | reversed |
| 2 (even) | [15, 7] | [15, 7] | same order, keep it |
Even-index levels are already right. Odd-index levels are exactly backwards. So the simplest plan: do normal level order, then go over the result and reverse every row whose index is odd.
4Building the logic from the example
Step 1: get the plain level order
Run the Part 0 code unchanged. For example 1 we get [[3], [9, 20], [15, 7]].
Step 2: walk over the rows with their index
Row 0 → index is even → leave it. Row 1 → odd → reverse [9, 20] into [20, 9]. Row 2 → even → leave it. Done.
Why the teacher calls this "brute force"
- Filling the list of levels already costs O(n) time and O(n) space.
- Then we make a second pass and reverse rows. Reversing a row costs time equal to its length.
- She looks at the worst case: what is the biggest row we could ever reverse? In a full binary tree (every level completely filled) the last level holds about half of all nodes. With 7 nodes, the last level has 4. So one reversal can cost about n/2, which is still O(n).
1 level 0: 1 node
/ \
2 3 level 1: 2 nodes
/ \ / \
4 5 6 7 level 2: 4 nodes ≈ n/2 (n = 7)
So the question becomes: can we avoid reversing altogether, and get the right order while we fill each row? That's Part B.
→ Not quite. Correction: every node sits in exactly one row, so if you add up the lengths of all the rows you reverse, the total is at most n. All the reversing together costs at most O(n). The brute force is O(n) + O(n) = O(n). It is still worse than Part B because it touches the values a second time, and that's the work Part B removes. In an interview, say "two passes, still linear, but we can do it in one".
5Approach steps
- If the root is None, return
[]. - Do normal level order traversal with a queue and the
sizetrick, collecting each level as a list. - After the BFS is over, loop over the rows with their index.
- If the index is odd, reverse that row in place.
- Return the list of rows.
6Code (Python)
from collections import deque
class Solution:
def zigzagLevelOrder(self, root):
ans = []
if root is None: # 0 nodes allowed
return ans
queue = deque([root])
while queue:
size = len(queue)
level = []
for i in range(size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
ans.append(level)
# second pass: flip every odd row
for idx in range(len(ans)):
if idx % 2 == 1:
ans[idx].reverse()
return ans7Code line by line
| line | what it means |
|---|---|
| if root is None: return ans | Empty tree → empty answer. Needed because the constraints allow 0 nodes. |
| queue = deque([root]) | Start BFS with the root in the queue. |
| size = len(queue) | Freeze how many nodes belong to the current level. |
| for i in range(size): | Take out exactly that many nodes: one whole level. |
| level.append(node.val) | Write the value in normal left-to-right order. |
| queue.append(node.left / right) | Children wait at the back of the queue for the next level. |
| ans.append(level) | The row is complete; add it to the answer. |
| if idx % 2 == 1: ans[idx].reverse() | The extra pass: odd rows are flipped to read right to left. |
8Dry run on example 1
9Complexity & remember
- Time O(n) for BFS + up to O(n) more for all the reversing (see the doubt above): linear, but two passes.
- Space O(n): the queue can hold a whole level (up to about n/2 nodes), and the answer holds all n values.
Part B · Optimal BFS with a deque
1The question, and 2 the constraints
Exactly the same as Part A: zigzag rows, 0 to 2000 nodes (so we keep the empty-tree check), small values. Only the way we build each row changes.
3Intuition: insert at the front to reverse for free
Say three values arrive in the order 1, 2, 3. Watch what happens with two ways of inserting:
| value arrives | insert at the back | insert at the front |
|---|---|---|
| 1 | [1] | [1] |
| 2 | [1, 2] | [2, 1] |
| 3 | [1, 2, 3] | [3, 2, 1] |
Inserting every new value at the front gives the list already reversed. We never call reverse; the order comes out right while we insert.
But a normal Python list is slow at inserting at the front (insert(0, x) shifts every item, O(k)). We need a structure where adding at both ends is O(1). That's a deque ("double-ended queue"). In Python, collections.deque has append (back) and appendleft (front), both O(1). The teacher mentions that in Java a LinkedList does the same job (addLast / addFirst), and in C++ and Python you use a deque.
4Building the logic
Each row becomes a deque instead of a list
The outer answer can stay a normal list. Only the inner row needs to be a deque, because that's where we insert at the front or the back.
How do we know which end to use? A flag
Keep a boolean left_to_right:
True→ this row reads left to right →level.append(node.val)(back).False→ this row reads right to left →level.appendleft(node.val)(front).
After a whole row is done (after the inner for loop), flip it: left_to_right = not left_to_right. If it was True it becomes False, and the other way round.
The two new pieces are the if/else around the insert and the flip after each row. Remove them and you're back to plain level order.
→ No. The teacher says any toggle works: a flag, an index that switches 0, 1, 0, 1, or the level number itself (even → back, odd → front). Pick the one you find clearest.
→ It depends on what the name means. Level 0 must read left to right, so the flag has to say "left to right" for the first row. The teacher starts it as True in the explanation and says False while typing the code; what matters is that the first row goes to the back. With the name
left_to_right, start with True.→ No, leave the queue alone. Children always go in left, then right, so the queue always holds each level in normal left-to-right order. We only change where we write the value in the row. If you also changed the child order, the two changes would get mixed up across levels and the rows would come out wrong.
→ To be safe, turn each row into a list when you store it:
ans.append(list(level)). That's an O(k) copy for a row of size k, n in total. It's just a format change, not a reversal.5Approach steps
- If the root is None, return
[]. - Put the root in the queue. Set
left_to_right = True. - While the queue isn't empty:
size = len(queue), and make an empty dequelevel. - Pop
sizenodes. For each one: ifleft_to_right, append its value at the back, else at the front. Push its children (left, then right). - After the row: store
list(level)in the answer and flip the flag. - Return the answer.
6Code (Python)
from collections import deque
class Solution:
def zigzagLevelOrder(self, root):
ans = []
if root is None:
return ans
queue = deque([root])
left_to_right = True
while queue:
size = len(queue)
level = deque() # the row is a deque now
for i in range(size):
node = queue.popleft()
if left_to_right:
level.append(node.val) # back: normal order
else:
level.appendleft(node.val) # front: reversed for free
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
ans.append(list(level))
left_to_right = not left_to_right # flip for the next row
return ans7Code line by line
| line | what it means |
|---|---|
| left_to_right = True | The first row (level 0) reads left to right. |
| level = deque() | A fresh row for this level that allows O(1) inserts at both ends. |
| level.append(node.val) | Left-to-right row: add at the back, so the order stays as popped. |
| level.appendleft(node.val) | Right-to-left row: add at the front, so the last popped ends up first. |
| queue.append(node.left) queue.append(node.right) | Children always go in left then right. The queue order never changes. |
| ans.append(list(level)) | Store the finished row as a plain list. |
| left_to_right = not left_to_right | Flip the direction for the next level. |
8Dry run on example 1
Yellow boxes = the level being popped. The second line shows the row deque as it grows.
On the fuller tree, level 2 would be popped as 4, 5, 6, 7 with the flag True → row [4, 5, 6, 7]. If there were a level 3, its values would go in with appendleft and come out reversed.
9Complexity & remember
- Time O(n): every node is pushed once and popped once.
appendandappendlefton a deque are O(1), and so is pushing a child. One pass, no reversing. - Space O(n): the queue holds at most one full level, which in a full tree is about n/2 nodes, still O(n).
- On LeetCode this ran faster than the brute force, because nothing is reversed after the BFS.
append, flag False → appendleft. Flip the flag after every level. Children always left, then right.Part C · Zigzag with DFS
1The question
Same output. The teacher adds this part because an interviewer may ask: "Can you also do it with DFS?" Her answer: yes, it's possible, but since the question itself is about levels, BFS is the better choice. As a beginner, you should still know both.
For this part she uses a slightly different tree, with an extra node 8 under 9, so that DFS has to come back to an old level later:
3 level 0 → [3]
/ \
9 20 level 1 ← [20, 9]
/ / \
8 15 7 level 2 → [8, 15, 7]
2What the constraints tell us
- 0 nodes allowed, and this is recursion → the base case
if root is None: returnis a must. It also handles the empty tree: nothing gets added, and we return[]. - n ≤ 2000 → the recursion depth is at most 2000 in the worst case (a straight-line tree). That's deeper than Python's default recursion limit (1000), so a truly skewed 2000-node tree could hit
RecursionError. One more reason BFS is preferred here.
3Intuition: give every call a level number
DFS does not go level by level. From 3 it dives to 9, then to 8, then comes back up and goes to 20. So how does it know that 9 and 20 belong in the same row?
The teacher's idea: pass the level number into every call. The root gets level 0. When a node calls its children, it passes its own level + 1. Each call on the call stack remembers its own level, so when we come back to 3 after finishing 9's side, 3 still knows it's level 0, and it gives 20 level 1.
Then we use that level as the index of the row in the answer: a node on level 2 goes into ans[2].
4Building the logic
Plain DFS with a level (what she draws first)
She first traces only the levels: 3 (level 0) → 9 (1) → 8 (2) → 8's children are None → return → 9's right is None → return → back to 3 → 20 (0 + 1 = 1) → 15 (2) → 7 (2). The levels come out correctly, but nothing is stored yet. So we add storing.
When do we need a new row? level == len(ans)
At the start, ans is empty. We don't know how many levels the tree has, so we create rows as we discover them:
- At 3: level 0,
len(ans)is 0 → they're equal → this is the first time we reach level 0 → add a new empty row. - At 9: level 1,
len(ans)is 1 → equal → first time on level 1 → add a row. - At 8: level 2, length 2 → add a row.
- At 20: level 1, but
len(ans)is now 3 → not equal → row 1 already exists, so don't create one; just use it.
==? Could level ever be bigger than len(ans)?→ No. To reach level k, DFS must pass through a node on level k−1 first, and that visit already created row k−1. So when we arrive at level k, there are at least k rows. The length is either equal to the level (a brand-new level) or bigger (the row exists). Equal is the only case where we must create one.
Get a handle on the row
After making sure the row exists, take it out once: cur = ans[level]. In Java the teacher has to type-cast it to a LinkedList; in Python, cur simply points to the same deque, so adding to cur adds to ans[level].
Back or front? Use the level itself
level % 2 == 0(even) →cur.append(val)- odd →
cur.appendleft(val)
Again each row is a deque, for the same reason as Part B: no reversing later.
→ Because we always call left before right. So among the nodes of any single level, DFS reaches them from the leftmost to the rightmost, the same order BFS pops them. On level 1 it meets 9 first, then 20. Putting 20 at the front gives [20, 9]. The visits to one row are spread out over time, but their order is still left to right.
5Approach steps
- Make an empty
ans, calldfs(root, 0), then return the rows as lists. - In
dfs(node, level): if node is None, return. - If
level == len(ans), append a new empty deque. cur = ans[level]. Even level → append at the back; odd → append at the front.- Call
dfs(node.left, level + 1), thendfs(node.right, level + 1).
6Code (Python)
from collections import deque
class Solution:
def zigzagLevelOrder(self, root):
ans = []
self.dfs(root, 0, ans)
return [list(row) for row in ans]
def dfs(self, root, level, ans):
if root is None: # base case: stops the recursion
return
if level == len(ans): # first visit to this level
ans.append(deque())
cur = ans[level]
if level % 2 == 0:
cur.append(root.val) # even level: back
else:
cur.appendleft(root.val) # odd level: front
self.dfs(root.left, level + 1, ans)
self.dfs(root.right, level + 1, ans)7Code line by line
| line | what it means |
|---|---|
| self.dfs(root, 0, ans) | Start at the root on level 0. The DFS only fills ans; it returns nothing. |
| return [list(row) for row in ans] | Turn each deque row into a list for the final answer. |
| if root is None: return | Base case. Stops at empty children, and makes an empty tree give []. |
| if level == len(ans): ans.append(deque()) | We've reached a level for the first time, so make its row. |
| cur = ans[level] | A handle on this level's row. |
| if level % 2 == 0: | Even rows read left to right → back. Odd rows → front. |
| self.dfs(root.left, level + 1, ans) self.dfs(root.right, level + 1, ans) | Children are one level deeper. Left first, so each row is filled left to right. |
8Dry run on the tree with 8
- dfs(3, 0): len(ans) = 0 = level → new row. Even → ans = [[3]].
- dfs(9, 1): len 1 = level → new row. Odd → front → ans = [[3], [9]].
- dfs(8, 2): len 2 = level → new row. Even → ans = [[3], [9], [8]].
- dfs(None, 3) twice (8's children) → return. 8 is done and leaves the stack. 9's right is None → return. 9 leaves the stack.
- Back in dfs(3, 0), which still remembers level 0 → dfs(20, 1): len is 3, not 1 → no new row. Odd → front → row 1 = [20, 9].
- dfs(15, 2): row exists. Even → back → row 2 = [8, 15]. Its children are None → return.
- dfs(7, 2): even → back → row 2 = [8, 15, 7]. Children None → return. 20 returns, then 3 returns. The stack is empty.
- Final: [[3], [20, 9], [8, 15, 7]] ✓
Each box remembers its own level. That's how 20 knows to use 0 + 1 = 1 even though 9's whole side ran in between.
→ BFS finishes one row completely before it starts the next: first all of row 0, then all of row 1, then row 2. DFS creates rows as it dives (rows 0, 1, 2 are all created on the first trip down the left side), then keeps coming back to fill old rows later (20 goes into row 1 after row 2 already exists). At the very end of the video the teacher's spoken words swap the two names, but this is the picture she draws.
9Complexity & remember
- Time O(n): each node is visited once; each insert is O(1).
- Space O(n) worst case. The call stack only holds the current path: in our tree the most it ever held was 3 calls, which is the number of levels. But if the tree is a straight chain, every node waits on the stack, so the worst case is O(n). (Plus the answer itself, which holds n values.)
- BFS and DFS have the same complexity here. The teacher still prefers BFS for any level-order question, because it matches the question directly.
level == len(ans) → new deque row. Even → append, odd → appendleft. Always call left before right.Part D · Revision page
| A · brute force | B · BFS + deque | C · DFS + level | |
|---|---|---|---|
| visits nodes | level by level (queue) | level by level (queue) | deep first (recursion) |
| row type | list | deque | deque |
| how odd rows get reversed | reverse() after BFS | appendleft while filling | appendleft while filling |
| direction decided by | row index % 2 | a flag, flipped per level | level % 2 |
| new row when | every loop round | every loop round | level == len(ans) |
| empty tree | check at top | check at top | base case handles it |
| time / space | O(n), two passes / O(n) | O(n), one pass / O(n) | O(n) / O(height), worst O(n) |
2. Brute force: do level order, then reverse odd rows.
3. Better: build each row in a deque,
append on left-to-right rows, appendleft on right-to-left rows.4. Flip a flag after each level; children always go in left, then right.
5. DFS works too: carry the level, create the row when
level == len(ans).✗ flipping the flag inside the inner loop instead of after it
✗ pushing children right-first on reversed rows (only the row insert changes)
✗ using
list.insert(0, x), which is O(k) per insert✗ in DFS, creating a new row on every visit instead of only when
level == len(ans)t1 = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) t2 = TreeNode(3, TreeNode(9, TreeNode(8)), TreeNode(20, TreeNode(15), TreeNode(7))) full = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3, TreeNode(6), TreeNode(7))) s = Solution() print(s.zigzagLevelOrder(t1)) # [[3], [20, 9], [15, 7]] print(s.zigzagLevelOrder(t2)) # [[3], [20, 9], [8, 15, 7]] print(s.zigzagLevelOrder(full)) # [[1], [3, 2], [4, 5, 6, 7]] print(s.zigzagLevelOrder(None)) # []
Based on this video: Binary Tree Zigzag Level Order Traversal | BFS & DFS