You already know how to build binary trees; now it's time to visit them methodically. In a list there's only one reasonable way to walk the elements (first to last); in a tree, by contrast, at every node you have to decide: do I process the node before going down, between its two children, or after coming back up? Each answer defines a traversal with a name of its own — preorder, inorder, and postorder — and to those we add the level-order traversal, which doesn't dive deep but sweeps the tree floor by floor with a queue. Choosing the right traversal is what separates "touching every node" from "solving the problem": in TaskFlow, exporting the hierarchy, computing accumulated hours, and painting the org chart level by level use three different traversals. This lesson also reunites old friends: recursion and the explicit stack from module 3, and the queue from module 4.
Contents
- The problem: in what order do I visit the nodes?
- Preorder, inorder, and postorder: the three depth-first traversals
- Detailed trace: the three traversals on the same tree
- Level-order traversal: the queue takes the stage
- Iterative versions with an explicit stack
- When to use each traversal (with TaskFlow as your guide)
The problem: in what order do I visit the nodes?
Traversing a tree means visiting each node exactly once. With the recursive definition of a binary tree (a node + left subtree + right subtree), traversing it requires doing three things: processing the Node, traversing the Left subtree, and traversing the Right subtree. The only thing that distinguishes the depth-first traversals is when the node gets processed (we'll always keep left before right):
| Traversal | Order | Mnemonic |
|---|---|---|
| Preorder | N, L, R | The node before its children |
| Inorder | L, N, R | The node between its children |
| Postorder | L, R, N | The node after its children |
All three are depth-first traversals (DFS): they sink down one branch to the bottom before touching the next. The fourth traversal, level-order (BFS, breadth-first), breaks the mold: it visits every node at depth 0, then every node at depth 1, and so on. A preview for module 7: DFS and BFS are actually general graph exploration strategies; here we meet them in their domesticated version, on trees, where they're simpler because there are no cycles.
Preorder, inorder, and postorder: the three depth-first traversals
The three functions are nearly identical — only one line moves. We'll use this tree throughout the lesson:
graph TD
A((10)) --> B((6))
A --> C((15))
B --> D((3))
B --> E((8))
C --> F((12))
C --> G((20))
class BinaryNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
root = BinaryNode(10)
root.left = BinaryNode(6)
root.right = BinaryNode(15)
root.left.left = BinaryNode(3)
root.left.right = BinaryNode(8)
root.right.left = BinaryNode(12)
root.right.right = BinaryNode(20)
def preorder(node, result):
if node is None:
return
result.append(node.value) # N first...
preorder(node.left, result) # ...then L...
preorder(node.right, result) # ...then R
def inorder(node, result):
if node is None:
return
inorder(node.left, result) # L first...
result.append(node.value) # ...N in the middle...
inorder(node.right, result) # ...then R
def postorder(node, result):
if node is None:
return
postorder(node.left, result) # L...
postorder(node.right, result) # ...R...
result.append(node.value) # ...and N last
for f in (preorder, inorder, postorder):
r = []
f(root, r)
print(f.__name__, r)
# preorder [10, 6, 3, 8, 15, 12, 20]
# inorder [3, 6, 8, 10, 12, 15, 20]
# postorder [3, 8, 6, 12, 20, 15, 10]Three observations before the trace:
- All three cost O(n) in time (each node is visited once) and O(height) in memory — the call stack from module 3 ends up stacking as many calls as the deepest branch is deep.
- In preorder, the root comes out first; in postorder, last. This detail will be key in the reconstruction exercise of 06-08.
- Look at the output of
inorder:[3, 6, 8, 10, 12, 15, 20]. Sorted. That's no coincidence: this tree satisfies (spoiler) the property that will define the next lesson, and the inorder of such a tree always comes out in ascending order. Consider this the formal announcement: in 06-04 this goes from curiosity to theorem and tool.
Detailed trace: the three traversals on the same tree
Let's follow preorder step by step, with the call stack in view (indentation = call depth):
preorder(10): appends 10 → [10]
preorder(6): appends 6 → [10, 6]
preorder(3): appends 3 → [10, 6, 3]
preorder(None) x2: return
preorder(8): appends 8 → [10, 6, 3, 8]
preorder(None) x2: return
preorder(15): appends 15 → [10, 6, 3, 8, 15]
preorder(12): appends 12 → [10, 6, 3, 8, 15, 12]
preorder(20): appends 20 → [10, 6, 3, 8, 15, 12, 20]Each call appends its value the moment it enters and then delegates. Now inorder on the left subtree (the pattern repeats higher up):
inorder(10)
inorder(6)
inorder(3)
inorder(None): return ← 3 has no left child
appends 3 → [3]
inorder(None): return
appends 6 ← only after exhausting its ENTIRE left subtree → [3, 6]
inorder(8)
appends 8 → [3, 6, 8]
appends 10 ← 10 waits for its whole left branch to finish → [3, 6, 8, 10]
inorder(15) ... → [3, 6, 8, 10, 12, 15, 20]And postorder: each node waits for both of its subtrees to finish — that's why 6 comes out after 3 and 8, and the root 10 comes out last of all. A visual trick for self-checking without tracing:
graph TD
A(("10 ③")) --> B(("6 ②"))
A --> C(("15 ⑥"))
B --> D(("3 ①"))
B --> E(("8 ④"))
C --> F(("12 ⑤"))
C --> G(("20 ⑦"))
Walk the outline of the tree starting at the root and going left: in preorder, jot down each node the first time you touch it on its left side; in inorder, when you pass underneath it (the numbers in the diagram are the inorder sequence); in postorder, the last time you touch it, on its right side. With a pencil and 20 seconds you can verify any traversal.
Level-order traversal: the queue takes the stage
To visit level by level (10, then 6 and 15, then 3-8-12-20), recursion doesn't help: depth isn't the order we want. The right tool is one you built in module 4 — a FIFO queue. The algorithm: enqueue the root; while the queue isn't empty, dequeue a node, process it, and enqueue its children. The children, entering at the back, wait their turn until the current level finishes.
from collections import deque # module 4's deque: O(1) at both ends
def level_order(root):
if root is None:
return []
result = []
queue = deque([root])
while queue:
node = queue.popleft() # the oldest one leaves (FIFO)
result.append(node.value)
if node.left:
queue.append(node.left) # children go to the back of the queue
if node.right:
queue.append(node.right)
return result
print(level_order(root)) # [10, 6, 15, 3, 8, 12, 20]Trace of the queue's state (left = next to leave):
| Step | Leaves | Enters | Queue after | Result |
|---|---|---|---|---|
| 1 | 10 | 6, 15 | [6, 15] | [10] |
| 2 | 6 | 3, 8 | [15, 3, 8] | [10, 6] |
| 3 | 15 | 12, 20 | [3, 8, 12, 20] | [10, 6, 15] |
| 4-7 | 3, 8, 12, 20 | — | [] | [10, 6, 15, 3, 8, 12, 20] |
And here a pending circle closes: the binaries_up_to(n) exercise from module 4 ("1, 10, 11, 100, 101...") was doing exactly this — each binary string s enqueued its "children" s+'0' and s+'1'; you were doing a level-order traversal of the perfect binary tree of all binary strings, without having seen a tree yet. This traversal is a BFS in every sense; in module 7 we'll generalize it to graphs, where it will need the visited set we previewed in 05-04 (in a tree it's unnecessary: with no cycles and a single parent, it's impossible to enqueue the same node twice). Cost: O(n) in time; in memory, O(maximum width) — in a perfect tree, the last level has ≈ n/2 nodes, so it can be O(n).
Iterative versions with an explicit stack
In module 3 you learned (with the nested subtasks and their RecursionError) that any recursion can be converted to iteration by managing the stack yourself. With very deep trees — a degenerate tree of 10,000 nodes exceeds Python's recursion limit — this conversion goes from elegance to necessity. Iterative preorder:
def iterative_preorder(root):
if root is None:
return []
result = []
stack = [root] # a Python list as a stack (module 3)
while stack:
node = stack.pop() # LIFO: the last one pushed leaves first
result.append(node.value)
if node.right: # right FIRST!...
stack.append(node.right)
if node.left: # ...so the left ends up on top
stack.append(node.left)
return result
print(iterative_preorder(root)) # [10, 6, 3, 8, 15, 12, 20] — same as the recursive oneThe counterintuitive detail: you push the right child before the left one, because the stack reverses — last in, first out, and we want to process the left one first. Compare with level_order: the same skeleton, swapping the queue for a stack. That symmetry (queue → level-order/BFS; stack → depth-first/DFS) is one of the most beautiful ideas in the course, and it will reappear verbatim in module 7.
Iterative inorder is subtler: you can't just process a node when you pop it, because its turn comes after its entire left subtree. The technique: slide down to the leftmost bottom, stacking the path, and on the way back up, process and jump to the right subtree.
def iterative_inorder(root):
result = []
stack = []
node = root
while stack or node is not None:
while node is not None: # 1) go down the left, stacking the path
stack.append(node)
node = node.left
node = stack.pop() # 2) no more left: this node's turn
result.append(node.value)
node = node.right # 3) and now its right subtree
return result
print(iterative_inorder(root)) # [3, 6, 8, 10, 12, 15, 20]The stack reproduces by hand what the call stack did on its own: remembering the ancestors still waiting to be processed. (Iterative postorder is even more convoluted — two stacks or visited-marking — and is rarely needed; knowing it exists is enough.)
When to use each traversal (with TaskFlow as your guide)
The general rule: ask yourself what needs to be done before processing a node.
| You need... | Traversal | Why | In TaskFlow |
|---|---|---|---|
| To process the parent before the children (create, copy, serialize) | Preorder | The parent must exist before hanging children | Exporting the project tree with indentation |
| The values in ascending order (in a BST) | Inorder | Left < node < right (we'll prove it in 06-04) | Task listing ordered by id |
| The children's results before processing the parent (aggregate, free, delete) | Postorder | The parent summarizes/depends on its subtrees | Accumulated hours per project |
| To process by closeness to the root | Level-order | The queue guarantees depth order | The "projects → categories → tasks" view, floor by floor |
Another classic postorder example: deleting a tree in languages with manual memory management — you must free the children before the parent so you don't lose their references. And arithmetic expression trees give you all three: preorder = prefix notation, inorder = the usual notation with parentheses, postorder = the reverse Polish notation we evaluated with a stack in module 3.
Two of TaskFlow's uses, in code. The preorder that exports the hierarchy (it's the show function from 06-01! — it processed the node and then the children: it was a preorder without knowing it) and the postorder that accumulates hours:
def accumulated_hours(node):
"""Hours of the subtree: its own plus those of all descendants.
Each node carries in value a dict with at least {'title', 'hours'}."""
if node is None:
return 0
children_total = sum(accumulated_hours(c) for c in [node.left, node.right])
return node.value["hours"] + children_total # the parent, AFTER its childrenThere's no way to know what a project costs without first adding up its parts: the nature of the problem forces postorder. (In 06-08 we'll do it at scale, with budgets over the generic tree.)
Common Mistakes and Tips
- Pushing the left child first in iterative preorder. The stack reverses the order: right first, left second. It's the number-one slip; if your iterative preorder comes out "mirrored", this is why.
- Using a stack for the level-order traversal (or a queue for DFS). The auxiliary structure is the traversal: stack → depth-first, queue → level-order. Mix them up and you get the other traversal by accident.
- Processing the node at the wrong moment. Computing accumulated totals in preorder forces contortions; in postorder they come out on their own. Before coding, decide which way information must flow: from parents to children (preorder, like the
depthparameter) or from children to parents (postorder, like the hours). - Trusting recursion with trees of unknown depth. A degenerate tree with thousands of nodes blows the call stack (
RecursionError, module 3). For data from external sources, the iterative version is the defensive one. - Tip: memorize this example tree's outputs (pre: root first; in: sorted; post: root last). Having one solved case in your head lets you validate any implementation in seconds.
Exercises
Exercise 1: level-order, with the levels separated
Modify level_order so it returns a list of lists, one per level: [[10], [6, 15], [3, 8, 12, 20]]. Hint: before draining each level, len(queue) tells you how many nodes it contains.
Exercise 2: maximum depth without recursion
Using the result of exercise 1 (or a queue directly), write iterative_height(root) that computes the tree's height without recursion. For the lesson's tree it must return 2.
Exercise 3: exporting TaskFlow in iterative preorder
On the generic tree (TreeNode from 06-01, with its children list), write an iterative export(root) with an explicit stack that returns the indented lines of the hierarchy (like show from 06-01, but without recursion and returning a list). Hint: push (node, depth) pairs, and push the children in reverse order to preserve their natural order.
Solutions
Solution 1
def levels_separated(root):
if root is None:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue) # everything in it NOW belongs to this level
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.value)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
print(levels_separated(root)) # [[10], [6, 15], [3, 8, 12, 20]]Comment: the key is the snapshot level_size = len(queue) — at the start of each while iteration, the queue contains exactly one complete level, and the for consumes it while enqueuing the next. Without the snapshot, the freshly enqueued children would mix into the current level. This "process in batches" pattern is an interview classic.
Solution 2
def iterative_height(root):
return len(levels_separated(root)) - 1
print(iterative_height(root)) # 2Comment: the height is the number of levels minus 1 (the root is level 0), and as a bonus the empty tree returns −1, consistent with 06-02's convention. With no recursion there's no possible RecursionError: this version survives the degenerate tree that would take down the recursive one.
Solution 3
def export(root):
if root is None:
return []
lines = []
stack = [(root, 0)] # (node, depth) pairs
while stack:
node, depth = stack.pop()
if isinstance(node.value, dict):
label = f"[{node.value['id']}] {node.value['title']}"
else:
label = node.value
lines.append(" " * depth + label)
for child in reversed(node.children): # reversed: the first ends up on top
stack.append((child, depth + 1))
return linesComment: two ideas combine. First: since there's no call stack to remember the depth, we carry it in the tuple — the manual equivalent of 06-01's depth parameter. Second: reversed(node.children) generalizes the "right before left" trick to n children. The result is identical line by line to show's, but immune to RecursionError: TaskFlow can now export hierarchies of any depth.
Conclusion
You now command the four canonical ways to visit a tree: preorder (node first — copy, serialize, export), inorder (node in the middle — with that sorted output whose secret is revealed in the next lesson), postorder (node last — aggregating and freeing from children to parents), and level-order (module 4's queue sweeping floor by floor — your first BFS, which will leap to graphs in module 7). And you know how to ground them with an explicit stack when depth threatens recursion. With the structure (06-02) and the traversals (06-03) in hand, it's time for the payoff: adding a placement rule to the binary tree — smaller to the left, larger to the right — and watching how, all at once, searching costs O(height), inorder hands you the data sorted, and the range queries the hash table couldn't answer ("priority between 1 and 3") finally have an owner. That's the binary search tree, and it's the next lesson.
Data Structures Course
Module 1: Introduction to Data Structures
- What Are Data Structures?
- The Importance of Data Structures in Programming
- Types of Data Structures
- Algorithmic Complexity and Big O Notation
- Arrays and Memory: the Foundation of Data Structures
Module 2: Lists
Module 3: Stacks
- Introduction to Stacks
- Basic Stack Operations
- Stack Implementation
- Stack Applications
- Stack Exercises
Module 4: Queues
- Introduction to Queues
- Basic Queue Operations
- Circular Queues
- Priority Queues
- Double-Ended Queues (Deques)
- Queue Exercises
Module 5: Hash Tables and Dictionaries
- Introduction to Hash Tables
- Hash Functions and Collision Resolution
- Dictionaries and Sets in Practice
- Hash Table Exercises
Module 6: Trees
- Introduction to Trees
- Binary Trees
- Tree Traversals
- Binary Search Trees
- AVL Trees
- B-Trees
- Heaps
- Tree Exercises
Module 7: Graphs
- Introduction to Graphs
- Graph Representation
- Graph Search Algorithms
- Shortest Path Algorithms
- Minimum Spanning Trees
- Graph Applications
- Graph Exercises
