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 · Before starting

The tree node

given by LeetCode, don't write this in the solution
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 None

What 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.

plain level order with BFS (the base we will edit)
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 ans

For 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:

LeetCode example 1
        3          → [3]
       / \
      9   20       ← [20, 9]
         /  \
        15   7     → [15, 7]
a fuller tree (to see it alternate)
         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

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 indexnormal level orderzigzag wantswhat 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"

         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.

Doubt: the teacher says the reversing is "O(n) inside O(n)", so O(n²). Is the brute force really O(n²)?
→ 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

  1. If the root is None, return [].
  2. Do normal level order traversal with a queue and the size trick, collecting each level as a list.
  3. After the BFS is over, loop over the rows with their index.
  4. If the index is odd, reverse that row in place.
  5. Return the list of rows.

6Code (Python)

Brute force: level order, then reverse odd rows
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 ans

7Code line by line

linewhat it means
if root is None: return ansEmpty 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

start3size = 1
level 0pop 3 → row [3] → push 9, 20
920size = 2
level 1pop 9 (no children), pop 20 → push 15, 7 → row [9, 20]
157size = 2
level 2pop 15, pop 7 (no children) → row [15, 7] → queue empty
pass 2ans = [[3], [9, 20], [15, 7]] → idx 1 is odd → reverse → [[3], [20, 9], [15, 7]]

9Complexity & remember

Remember the brute forceNormal level order, then reverse rows with odd index. Easy to explain first in an interview, then improve.

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 arrivesinsert at the backinsert 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:

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.

Doubt 1: do I have to use a flag?
→ 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.
Doubt 2: should the flag start as True or False?
→ 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.
Doubt 3: on right-to-left rows, should I push the children right child first?
→ 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.
Doubt 4: LeetCode expects a list of lists. Can I return deques?
→ 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

  1. If the root is None, return [].
  2. Put the root in the queue. Set left_to_right = True.
  3. While the queue isn't empty: size = len(queue), and make an empty deque level.
  4. Pop size nodes. For each one: if left_to_right, append its value at the back, else at the front. Push its children (left, then right).
  5. After the row: store list(level) in the answer and flip the flag.
  6. Return the answer.

6Code (Python)

Optimal BFS: deque row, insert at back or front
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 ans

7Code line by line

linewhat it means
left_to_right = TrueThe 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_rightFlip 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.

level 03flag = True (back)
pop 3 → row [3] → push 9, 20 → store [3] → flag becomes False
level 1920flag = False (front)
pop 9 → appendleft → row [9]
pop 20 → appendleft → row [20, 9] → push 15, 7 → store [20, 9] → flag becomes True
level 2157flag = True (back)
pop 15 → row [15] → pop 7 → row [15, 7] → store → queue empty
end[[3], [20, 9], [15, 7]] with no reverse call anywhere

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

Remember optimal BFSPlain level order + a flag. Row is a deque: flag True → 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

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:

Doubt 1: why check ==? 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

Again each row is a deque, for the same reason as Part B: no reversing later.

Doubt 2: why does inserting at the front still give right-to-left in DFS, when DFS jumps around?
→ 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

  1. Make an empty ans, call dfs(root, 0), then return the rows as lists.
  2. In dfs(node, level): if node is None, return.
  3. If level == len(ans), append a new empty deque.
  4. cur = ans[level]. Even level → append at the back; odd → append at the front.
  5. Call dfs(node.left, level + 1), then dfs(node.right, level + 1).

6Code (Python)

Zigzag with DFS and a level number
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

linewhat 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: returnBase 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

  1. dfs(3, 0): len(ans) = 0 = level → new row. Even → ans = [[3]].
  2. dfs(9, 1): len 1 = level → new row. Odd → front → ans = [[3], [9]].
  3. dfs(8, 2): len 2 = level → new row. Even → ans = [[3], [9], [8]].
  4. 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.
  5. 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].
  6. dfs(15, 2): row exists. Even → back → row 2 = [8, 15]. Its children are None → return.
  7. dfs(7, 2): even → back → row 2 = [8, 15, 7]. Children None → return. 20 returns, then 3 returns. The stack is empty.
  8. Final: [[3], [20, 9], [8, 15, 7]] ✓
stack at step 3 (deepest)
dfs(3, 0)dfs(9, 1)dfs(8, 2)
stack at step 5
dfs(3, 0)dfs(20, 1)
stack at step 7
dfs(3, 0)dfs(20, 1)dfs(7, 2)

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.

Doubt 3: what is the real difference in how BFS and DFS fill the rows?
→ 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

Remember DFS zigzagPass level + 1 to children. level == len(ans) → new deque row. Even → append, odd → appendleft. Always call left before right.

Part D · Revision page

A · brute forceB · BFS + dequeC · DFS + level
visits nodeslevel by level (queue)level by level (queue)deep first (recursion)
row typelistdequedeque
how odd rows get reversedreverse() after BFSappendleft while fillingappendleft while filling
direction decided byrow index % 2a flag, flipped per levellevel % 2
new row whenevery loop roundevery loop roundlevel == len(ans)
empty treecheck at topcheck at topbase case handles it
time / spaceO(n), two passes / O(n)O(n), one pass / O(n)O(n) / O(height), worst O(n)
If you remember only 5 lines 1. Zigzag = level order where odd rows are reversed.
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).
Mistakes to avoid ✗ forgetting the empty-tree check in BFS (0 nodes allowed)
✗ 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)
test it yourself (paste under any of the solutions above)
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