Trees & Binary Trees
Move from linear structures to hierarchical ones, and learn the traversal orders every tree algorithm builds on
Introduction
Arrays, linked lists, stacks, and queues have all been fundamentally linear: one element leads to the next. Hash tables loosened that by letting you jump straight to a value through its key, but the storage underneath was still one flat array of buckets. A tree breaks the mold entirely. It's a hierarchical structure where each element can branch into several others, exactly like a file system's folders, an HTML document's DOM, or an org chart. A binary tree, where every node has at most two children, is the simplest and most common variant, and it's the foundation the rest of this course builds on: binary search trees, heaps, and much of graph theory all start here.
Tree Terminology
In general, a tree lets any node have any number of children, a file system folder can contain any number of files and subfolders, an HTML element can have any number of child elements, an org chart's manager can have any number of direct reports. The vocabulary below (root, parent, child, leaf, depth, height, subtree) applies to every tree, general or binary.
A binary tree is the special case where every node has at most two children, conventionally called left and right. That restriction is what the rest of this lesson, and most of the tree structures later in this course, focus on, since it keeps the code simple while still capturing everything hierarchical structures are good for:
A Small Binary Tree
Figure 1: Node 1 is the root, nodes 3, 4, and 5 are leaves
| Term | Meaning |
|---|---|
| Root | The single top-level node with no parent (node 1 above) |
| Parent / Child | 1 is the parent of 2 and 3; 2 and 3 are its children |
| Leaf | A node with no children (3, 4, and 5) |
| Depth of a node | Number of edges from the root down to that node |
| Height of a tree | The depth of its deepest leaf |
| Subtree | Any node together with all of its descendants, itself a valid tree |
Binary Trees and the Node Class
A binary tree node looks a lot like Lesson 3's linked list node, except instead of one next pointer, it has two: left and right.
class TreeNode:
def __init__(self, value: int) -> None:
self.value = value
self.left: "TreeNode | None" = None
self.right: "TreeNode | None" = None# 1 # / \ # 2 3 # / \ # 4 5 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.left.left = TreeNode(4) root.left.right = TreeNode(5)
Depth-First Traversals
"Visiting every node" isn't a single operation, the order you visit them in matters, and different orders serve different purposes. Depth-first traversals go as deep as possible down one branch before backtracking, and they map almost directly onto Lesson 5's recursion: the base case is an empty subtree, the recursive case processes the two children.
def preorder(node: "TreeNode | None") -> list[int]:
if node is None:
return []
# Root first, then everything on the left, then everything on the right
return [node.value] + preorder(node.left) + preorder(node.right)def inorder(node: "TreeNode | None") -> list[int]:
if node is None:
return []
# Left first, then the root, then the right
return inorder(node.left) + [node.value] + inorder(node.right)def postorder(node: "TreeNode | None") -> list[int]:
if node is None:
return []
# Both children before the root itself
return postorder(node.left) + postorder(node.right) + [node.value]print(preorder(root)) print(inorder(root)) print(postorder(root))
Expected Output:
[1, 2, 4, 5, 3] [4, 2, 5, 1, 3] [4, 5, 2, 3, 1]
The small number under each node is the order that traversal actually visits it. Same tree, same recursive shape every time, only the moment the root is recorded moves, and that alone produces three different sequences:
Preorder (Root, L, R)
Visit order: 1 → 2 → 4 → 5 → 3
Inorder (L, Root, R)
Visit order: 4 → 2 → 5 → 1 → 3
Postorder (L, R, Root)
Visit order: 4 → 5 → 2 → 3 → 1
Only the Position of the Root Changes
Compare the three functions: the recursive structure is identical, only where node.value gets inserted relative to the two recursive calls changes. This is a pattern you'll see again and again in tree algorithms.
Breadth-First Traversal (Level Order)
Instead of diving deep, a level order traversal visits every node on one level before moving to the next. That requires remembering which nodes are "next in line", exactly the FIFO behavior a queue provides, so Lesson 4's deque does the work here instead of the call stack.
from collections import deque
def level_order(root: "TreeNode | None") -> list[int]:
if root is None:
return []
result: list[int] = []
queue: deque[TreeNode] = deque([root]) # Lesson 4's queue, not a stack
while queue:
node = queue.popleft()
result.append(node.value)
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
return resultprint(level_order(root))
Expected Output:
[1, 2, 3, 4, 5]
Each node is enqueued once and dequeued once, so level order traversal is O(n), the same as every DFS order, just with a different data structure driving it and a different visiting order.
Recursive Tree Algorithms
Most questions about a tree, how tall is it, how many nodes does it have, is it balanced, have the same shape: solve the trivial case for an empty tree, then combine the answers from the left and right subtrees.
def height(node: "TreeNode | None") -> int:
if node is None:
return -1 # convention: an empty tree has height -1
return 1 + max(height(node.left), height(node.right))
def count_nodes(node: "TreeNode | None") -> int:
if node is None:
return 0
return 1 + count_nodes(node.left) + count_nodes(node.right)print(height(root)) print(count_nodes(root))
Expected Output:
2 5
height answers "how many edges to the deepest leaf", count_nodes answers "how many nodes total". Both visit every node exactly once, O(n), and both are only a few lines because the recursion does the traversal for free.
Bonus: Inverting a Binary Tree
Famously the subject of a viral interview-question controversy, inverting a tree means mirroring it: every node's left and right children are swapped, all the way down.
def invert(node: "TreeNode | None") -> "TreeNode | None":
if node is None:
return None
# Swap the children, then invert each of the (now swapped) subtrees
node.left, node.right = invert(node.right), invert(node.left)
return nodeprint(preorder(root)) invert(root) print(preorder(root))
Expected Output:
[1, 2, 4, 5, 3] [1, 3, 2, 5, 4]
Notice the swap happens on the way down the recursion, each node hands its (already-inverted) subtrees to its parent, which just needs to place them on the opposite sides. Like the traversals above, it's O(n): every node is visited, and swapped, exactly once.
Key Takeaways
- Trees are hierarchical, not linear - each node can branch into multiple children instead of pointing to a single next element
- Preorder, inorder, and postorder are the same recursive DFS shape, they only differ in where the root is visited relative to its children
- Level order traversal needs a queue, not the call stack, to visit nodes breadth-first instead of depth-first
- Every full traversal is O(n) - each node is visited exactly once, regardless of which order you choose
- Recursive tree algorithms follow one template: handle the empty case, then combine the results from the left and right subtrees