Chapter 9: Trees
Introduction
Learning Outcomes
Explain the terms root, parent, child, leaf, depth, and height.
Represent trees with linked nodes and with arrays.
Carry out preorder, inorder, postorder, and level-order traversals, and trace recursion on the call stack.
Compare general and binary trees by their applications and the cost of their operations.
The Tree Concept
Trees
- A nonlinear, hierarchical data structure.
- Made of nodes and the edges that connect them.
- A single root sits at the top; trees are drawn upside down, growing downward from the root.
- Every node except the root has exactly one parent, so there are no cycles.
- A node may have zero or more children.
The Tree Concept
Trees in Everyday Life: Hierarchies
The Tree Concept
Is It a Tree?
Is this structure a tree? It has 16 nodes.
Yes. It is connected and acyclic, with 15 edges for 16 nodes. Any node (say, I) can be chosen as the root and the structure drawn as a tree.
Not trees: structures with a cycle, a node with two parents, or disconnected pieces.
The Tree Concept
Basic Parts: Root, Internal Node, Leaf
The root is an internal node too (if it has children). In a one-node tree the root is also a leaf.
The Tree Concept
Tree Relationships
| Term | Meaning (in the tree on the right) |
|---|---|
| Parent | The node directly above: D is H's parent |
| Child | A node directly below: H, I, J → children of D |
| Sibling | Nodes with the same parent: H, I, J |
| Ancestor | Nodes on the path to the root: M's ancestors are H, D, A |
| Descendant | Nodes in the subtree: D's descendants are H, I, J, M |
The Tree Concept
The Terms on a Tree
Root: the only node with no parent → A.
Parent / child: D is the parent of H, I, and J; H, I, J are D's children.
Siblings: H, I, J (their parent is D). F and G are not siblings.
Leaves: K, L, F, G, M, I, J (nodes without children).
Internal nodes: A, B, C, D, E, H (nodes with at least one child).
Ancestors: M's ancestors are H, D, A. Conversely, M is a descendant of H, D, and A.
Degree = number of children: A 3, B 2, C 1, D 3 … The degree of the tree = the largest degree = 3.
Depth: the number of edges from the root to the node. E has depth 2 (A→B→E).
Height: the number of edges from the root to the farthest leaf = 3 (A→B→E→K).
Subtree: D and all its descendants (H, I, J, M) form a tree of their own.
The Tree Concept
Degree, Depth, Height
| Measure | Definition | In the previous tree |
|---|---|---|
| Degree | The number of children of a node (its number of subtrees) | D → 3 |
| Depth | The number of edges on the path from the root to the node; the root has depth 0 | E → 2 |
| Level | The set of nodes at the same depth | level 2: E … J |
| Height of a node | The number of edges from the node down to its farthest leaf; a leaf has height 0 | D → 2 |
| Height of the tree | The height of the root = the largest depth | 3 |
The Tree Concept
Depth and Height
| Node | Height | Depth |
|---|---|---|
| A | 3 | 0 |
| B | 1 | 1 |
| C | 0 | 1 |
| D | 1 | 1 |
| E | 2 | 1 |
| F | 0 | 2 |
| G | 0 | 2 |
| H | 0 | 2 |
| I | 0 | 2 |
| J | 1 | 2 |
| K | 0 | 3 |
The Tree Concept
Paths and Subtrees
- Path: a sequence of nodes in which consecutive nodes are joined by edges.
p = (v₀, e₁, v₁, …, ek, vk) start(p) = v₀,end(p) = vk,length(p) = k(the number of edges).- In a tree, any two nodes are joined by exactly one simple path; a second path would create a cycle.
- Subtree: a node together with all its descendants; it is a tree itself. Example: a folder and everything under it.
The Tree Concept
Basic Kinds of Trees
| Kind | Property |
|---|---|
| General tree | No limit on the number of children a node may have. |
| k-ary tree | Each node has at most k children. |
| Binary tree | Each node has at most two children: left and right. |
| Binary search tree (BST) | A binary tree in which, at every node, keys in the left subtree are smaller and keys in the right subtree larger. |
The Tree Concept
The Tree Abstract Data Type (Tree ADT)
public interface Tree<E> extends Iterable<E> { Position<E> root(); Position<E> parent(Position<E> p); Iterable<Position<E>> children(Position<E> p); int numChildren(Position<E> p); boolean isInternal(Position<E> p); // has a child boolean isExternal(Position<E> p); // a leaf boolean isRoot(Position<E> p); // no parent int size(); boolean isEmpty(); Iterable<Position<E>> positions(); // all nodes } // Position<E>: E getElement();
The binary tree ADT adds left(p), right(p), and sibling(p) (as in Goodrich et al.).
A position is an abstraction of a "place" that hides the node itself: the user sees only the element and the ADT methods.
The Tree Concept · Step through the code
Code Trace: depth(G) and height(B) with the ADT
depth(G): G is not the root → call depth(E) on its parent, E (moving up).
depth(E): E is not the root → call depth(B) on its parent, B (moving up).
depth(B): B is not the root → call depth(A) on its parent, A (moving up).
depth(A): A is the root → return 0. This is the base case; now the frames start returning.
depth(B) = 1 + 0 = 1 is returned.
depth(E) = 1 + 1 = 2 is returned.
depth(G) = 1 + 2 = 3 is returned. G has depth 3: 3 edges from the root to G (A–B–E–G). Cost: O(d + 1).
Now height(B): the recursion goes down, through the children (depth looks up, height looks down).
height(B): h = 0. Children: D, E. Recurse on the first child, D.
height(D): h = 0; D has no children, so the loop never runs → return 0 (a leaf).
D returned 0: h = max(0, 1 + 0) = 1. Next child: E.
height(E): h = 0. Children: G. Recurse on the first child, G.
height(G): h = 0; G has no children, so the loop never runs → return 0 (a leaf).
G returned 0: h = max(0, 1 + 0) = 1. No children left → return 1.
E returned 1: h = max(1, 1 + 1) = 2. No children left → return 2. Height(B) = 2 (B–E–G); each node is visited once.
int depth(Position<E> p) { if (isRoot(p)) return 0; return 1 + depth(parent(p)); } int height(Position<E> p) { int h = 0; for (Position<E> c : children(p)) h = Math.max(h, 1 + height(c)); return h; }
Binary Trees
Binary Trees
- Each node has at most two children: a left child and a right child.
- Position matters: a tree whose only child is a left child differs from one whose only child is a right child.
- Recursive definition: a binary tree is either empty or consists of a root and two binary trees (the left and right subtrees).
The chain on the left is a valid binary tree too (a skewed tree): nodes need not be spread out evenly.
Binary Trees
Full, Complete, and Perfect
Binary Trees
Which One? Full, Complete, or Perfect?
| Full | yes |
| Complete | no |
| Perfect | no |
Every node has 0 or 2 children → full. But the bottom level hangs under E while the space under C is empty → not complete.
| Full | no |
| Complete | yes |
| Perfect | no |
The last level (8, 9, 10) fills from left to right with no gaps → complete. Node 5 has only one child (10) → not full.
| Full | no |
| Complete | no |
| Perfect | no |
Node 5 has one child → not full. Position 10 is empty while 11 is filled → not complete. Neither complete nor full.
| Full | yes |
| Complete | yes |
| Perfect | yes |
Every internal node has 2 children and all leaves have the same depth → perfect (and therefore full and complete).
Binary Trees
Properties of Binary Trees
| Property | Formula | Example |
|---|---|---|
| Most nodes at depth d | 2d | d = 3 → at most 8 nodes |
| Most nodes in a tree of height h | 2h+1 − 1 | h = 3 → 15 (perfect tree) |
| Fewest nodes in a tree of height h | h + 1 | skewed (chain) tree |
| Height of a tree with n nodes | ⌊log2 n⌋ ≤ h ≤ n − 1 | n = 15 → 3 ≤ h ≤ 14 |
| Number of edges in a tree with n nodes | n − 1 | each node has one parent |
| Number of leaves in a full binary tree | internal nodes + 1 | 7 nodes → 3 internal, 4 leaves |
Binary Trees · Step by step
Full Binary Trees: Why Leaves = Internal Nodes + 1?
A one-node tree: the root is a leaf (L). i = 0, l = 1 → l = i + 1.
Give a leaf two children (a full tree allows 0 or 2): the leaf becomes an internal node (i + 1) and two new leaves appear (l − 1 + 2 = l + 1). The difference stays the same.
Give a leaf two children (a full tree allows 0 or 2): the leaf becomes an internal node (i + 1) and two new leaves appear (l − 1 + 2 = l + 1). The difference stays the same.
Give a leaf two children (a full tree allows 0 or 2): the leaf becomes an internal node (i + 1) and two new leaves appear (l − 1 + 2 = l + 1). The difference stays the same.
Each expansion adds 1 to both i and l; since l − i = 1 at the start, every full binary tree has l = i + 1 (by induction). Consequently n = 2i + 1, so n is always odd.
I: internal node, L: leaf; orange: the leaves added in the last step. This property is used in expression trees (every operator has 2 operands) and Huffman trees.
Representations
Array Representation
Nodes are placed in the array level by level, left to right. The root goes at index 1 in a 1-based array, at index 0 in a 0-based one. For the node at index i:
| 1-based | 0-based | |
|---|---|---|
| Left child | 2i | 2i + 1 |
| Right child | 2i + 1 | 2i + 2 |
| Parent | ⌊i / 2⌋ | ⌊(i − 1) / 2⌋ |
Example: E (i = 5) → children at 10 and 11 (empty), parent at ⌊5/2⌋ = 2 (B).
No references are stored; parent and child are reached with O(1) arithmetic. Ideal for complete binary trees (such as heaps).
Representations · Step through the code
Code Trace: Index Arithmetic in the Array Representation
left(2) = 2·2 = 4 → a[4] = D, B's left child (the array is 1-based; a[0] is empty). No references: positions come from arithmetic.
right(2) = 2·2 + 1 = 5 → a[5] = E. The right child always sits right next to the left child.
parent(2) = 2 / 2 = 1 → a[1] = A (the root).
D (i = 4): children 8 → H and 9 → I.
E (i = 5): 10 and 11 > n = 9 → outside the array, so E has no children (a leaf). The check is 2i ≤ n.
parent(9) = 9 / 2 = 4: integer division rounds down (⌊4.5⌋ = 4) → D. Since 8 and 9 are siblings, both give 4.
pathToRoot(a, 9): follow the chain of parents from I up to the root.
9 ≥ 1 → the loop continues; a[9] = I is printed.
i = parent(9) = 4 → D.
4 ≥ 1 → the loop continues; a[4] = D is printed.
i = parent(4) = 2 → B.
2 ≥ 1 → the loop continues; a[2] = B is printed.
i = parent(2) = 1 → A.
1 ≥ 1 → the loop continues; a[1] = A is printed.
i = parent(1) = 0 (the root has no parent).
0 ≥ 1 is false → the loop ends. Output: I D B A. Each step halves the index → O(log n) (as many steps as the depth).
int left(int i) { return 2 * i; } int right(int i) { return 2 * i + 1; } int parent(int i) { return i / 2; } void pathToRoot(char[] a, int i) { // from i up to the root while (i >= 1) { System.out.print(a[i] + " "); i = parent(i); } }
Representations
Wasted Space in Arrays
- In a skewed tree the indices grow exponentially: 1, 2, 4, 8, 16.
- 5 nodes need 16 cells, leaving 11 empty. Skewed to the right, the indices would be 1, 3, 7, 15, 31 → 31 cells.
- In general, a tree of n nodes may need up to 2n − 1 cells.
Array representation
- No references, fast index arithmetic
- No gaps for complete trees
But
- Large waste for sparse or skewed trees
- Fixed size; growing means copying
Representations
Linked Representation
- Each tree node is an object: data plus references to its left and right children.
- A missing child is a
nullreference. - The size is dynamic; memory is allocated only for nodes that exist.
- Reaching a node means starting at the root and following references.
public class TreeNode { int data; TreeNode left; // left child (null if none) TreeNode right; // right child (null if none) public TreeNode(int data) { this.data = data; // left = right = null } }
// build a small tree rooted at 9 TreeNode root = new TreeNode(9); root.left = new TreeNode(2); root.right = new TreeNode(3); root.left.left = new TreeNode(4);
Representations
Linked Representation: Example
parent reference.Representations
Array or Linked?
| Criterion | Array representation | Linked representation |
|---|---|---|
| Memory | Minimal for complete trees; up to 2n − 1 cells for sparse ones | 2 extra references per node; no waste |
| Child / parent access | O(1) by index arithmetic | Child O(1); the parent needs an extra reference |
| k-th node on level i | O(1): index 2i + k − 1 (1-based, k ≥ 1) | Requires walking down from the root |
| Growth | When the array fills up, it is grown by copying | Adding a node is O(1) (if its position is known) |
| Best suited for | Complete binary trees (heaps) | General trees whose shape changes (BSTs, expression trees) |
Representations
Representing General Trees
When a node can have any number of children, two approaches are common:
// 1) List of children class GeneralNode { int data; List<GeneralNode> children = new ArrayList<>(); } // 2) First child – next sibling class SiblingNode { int data; SiblingNode firstChild; SiblingNode nextSibling; }
In the second approach every node again holds two references: first child (down) and next sibling (across). This lets any general tree be stored like a binary tree.
Left: a general tree; right: its first child (solid arrow) – next sibling (dashed arrow) representation.
Traversals
Tree Traversals
| Traversal | Order | Output | Typical use |
|---|---|---|---|
| Preorder | root → left → right | 1 2 4 5 3 6 7 | Copying a tree, prefix expressions |
| Inorder | left → root → right | 4 2 5 1 6 3 7 | Visiting a BST in sorted order |
| Postorder | left → right → root | 4 5 2 6 7 3 1 | Deletion, computing size/height |
| Level order | level by level, left to right | 1 2 3 4 5 6 7 | Level-based operations |
Traversals
Preorder Traversal
- Visit the root.
- Traverse the left subtree in preorder.
- Traverse the right subtree in preorder.
Use: copying a tree, writing the nodes to an array, prefix expressions.
visit(root); preorder(root.left); preorder(root.right);
Visit the root, 1; then move to the left subtree.
Visit 2; then its left subtree.
4 is a leaf: visit it; it has no children.
2's right subtree: 5.
The left subtree is done; 1's right subtree: 3.
3's left child is 6.
3's right child is 7. Output: 1 2 4 5 3 6 7
Traversals
Inorder Traversal
- Traverse the left subtree in inorder.
- Visit the root.
- Traverse the right subtree in inorder.
Use: listing the keys of a binary search tree in increasing order, infix expressions.
inorder(root.left); visit(root); inorder(root.right);
Keep going left from the root; the first node visited is the leftmost one: 4.
4's subtree is done → its parent, 2.
2's right subtree: 5.
The left subtree is complete → the root, 1.
In the right subtree, again go all the way left: 6.
After 6, its parent, 3.
Finally 7. Output: 4 2 5 1 6 3 7
Traversals
Postorder Traversal
- Traverse the left subtree in postorder.
- Traverse the right subtree in postorder.
- Visit the root.
Use: deleting a tree (children first), computing size/height, postfix expressions.
postorder(root.left); postorder(root.right); visit(root);
The leftmost leaf, 4, is visited first.
Then 2's right child, 5.
Both of 2's subtrees are done → 2.
1's right subtree: 6 first.
Then 7.
3's children are done → 3.
The root is visited last: 1. Output: 4 5 2 6 7 3 1
Traversals · Step by step
Three Traversals, One Walk: the Euler Tour
Walk around the tree, starting to the left of the root and keeping your left hand on the edges (an Euler tour). You pass each node three times: on its left, below it, and on its right.
Write a node down when you pass on its left (the first encounter) and you get preorder: A B D E C.
Write it down when you pass below it (left subtree done, right not yet started) and you get inorder: D B E A C.
Write it down when you pass on its right (both subtrees done) and you get postorder: D E B C A. The three traversals are different moments of the same walk, which takes O(n).
Traversals
Level-Order Traversal
- Starts at the root, scans each level from left to right, then moves down a level.
- It is breadth-first search (BFS) applied to a tree.
- Uses a queue (FIFO) instead of recursion: the children of the node that arrived first are processed first.
Use: level-based operations (the sum of each level, the nearest leaf), printing a tree level by level.
Traversals · Step through the code
Recursive Preorder and the Call Stack
preorder(9) is called (the root). Not null → print the root, 9, then the subtrees.
preorder(2) is called (left child of 9). Not null → print the root, 2, then the subtrees.
preorder(4) is called (left child of 2). Not null → print the root, 4, then the subtrees.
preorder(4.left) = preorder(null): base case; return at once, since an empty subtree needs no work.
preorder(4.right) = preorder(null): base case; return at once, since an empty subtree needs no work.
preorder(4) returns (line 8); its frame is popped and we are back in preorder(2).
preorder(2.right) = preorder(null): base case; return at once, since an empty subtree needs no work.
preorder(2) returns (line 8); its frame is popped and we are back in preorder(9).
preorder(3) is called (right child of 9). Not null → print the root, 3, then the subtrees.
preorder(3.left) = preorder(null): base case; return at once, since an empty subtree needs no work.
preorder(3.right) = preorder(null): base case; return at once, since an empty subtree needs no work.
preorder(3) returns (line 8); its frame is popped and we are back in preorder(9).
preorder(9) returns (line 8); the stack is empty. Output: 9 2 4 3 — each node once, at most h + 2 = 4 frames.
public void preorder(TreeNode root) { if (root == null) { return; } System.out.print(root.data + " "); preorder(root.left); preorder(root.right); }
Traversals · Step through the code
Recursive Inorder and the Call Stack
inorder(9) is called (the root). 9 is not printed yet: the left subtree comes first.
inorder(2) is called (left child of 9). 2 is not printed yet: the left subtree comes first.
inorder(4) is called (left child of 2). 4 is not printed yet: the left subtree comes first.
inorder(4.left) = inorder(null): base case; return at once, since an empty subtree needs no work.
Left subtree done; back in inorder(4) → print 4. The right subtree is next.
inorder(4.right) = inorder(null): base case; return at once, since an empty subtree needs no work.
inorder(4) returns (line 8); its frame is popped and we are back in inorder(2).
Left subtree done; back in inorder(2) → print 2. The right subtree is next.
inorder(2.right) = inorder(null): base case; return at once, since an empty subtree needs no work.
inorder(2) returns (line 8); its frame is popped and we are back in inorder(9).
Left subtree done; back in inorder(9) → print 9. The right subtree is next.
inorder(3) is called (right child of 9). 3 is not printed yet: the left subtree comes first.
inorder(3.left) = inorder(null): base case; return at once, since an empty subtree needs no work.
Left subtree done; back in inorder(3) → print 3. The right subtree is next.
inorder(3.right) = inorder(null): base case; return at once, since an empty subtree needs no work.
inorder(3) returns (line 8); its frame is popped and we are back in inorder(9).
inorder(9) returns (line 8); the stack is empty. Output: 4 2 9 3 — each node once, at most h + 2 = 4 frames.
public void inorder(TreeNode root) { if (root == null) { return; } inorder(root.left); System.out.print(root.data + " "); inorder(root.right); }
Traversals · Step through the code
Recursive Postorder and the Call Stack
postorder(9) is called (the root). The root waits until last: left subtree first.
postorder(2) is called (left child of 9). The root waits until last: left subtree first.
postorder(4) is called (left child of 2). The root waits until last: left subtree first.
postorder(4.left) = postorder(null): base case; return at once, since an empty subtree needs no work.
postorder(4.right) = postorder(null): base case; return at once, since an empty subtree needs no work.
postorder(4): both subtrees are done → print 4 now.
postorder(4) returns (line 8); its frame is popped and we are back in postorder(2).
postorder(2.right) = postorder(null): base case; return at once, since an empty subtree needs no work.
postorder(2): both subtrees are done → print 2 now.
postorder(2) returns (line 8); its frame is popped and we are back in postorder(9).
postorder(3) is called (right child of 9). The root waits until last: left subtree first.
postorder(3.left) = postorder(null): base case; return at once, since an empty subtree needs no work.
postorder(3.right) = postorder(null): base case; return at once, since an empty subtree needs no work.
postorder(3): both subtrees are done → print 3 now.
postorder(3) returns (line 8); its frame is popped and we are back in postorder(9).
postorder(9): both subtrees are done → print 9 now.
postorder(9) returns (line 8); the stack is empty. Output: 4 2 3 9 — each node once, at most h + 2 = 4 frames.
public void postorder(TreeNode root) { if (root == null) { return; } postorder(root.left); postorder(root.right); System.out.print(root.data + " "); }
Traversals · Step through the code
Preorder Traversal with a Stack
Push the root. The stack stands in for the call stack of the recursive version.
pop() returns 1, printed at once (preorder: root first).
Push the right child, 3: right goes first, so left ends up on top and is processed first (LIFO).
Push the left child, 2. It is now on top, so the next pop() returns it.
pop() returns 2, printed at once (preorder: root first).
Push the right child, 5: right goes first, so left ends up on top and is processed first (LIFO).
Push the left child, 4. It is now on top, so the next pop() returns it.
pop() returns 4, printed at once (preorder: root first). 4 is a leaf: no children to push.
pop() returns 5, printed at once (preorder: root first). 5 is a leaf: no children to push.
pop() returns 3, printed at once (preorder: root first).
Push the right child, 7: right goes first, so left ends up on top and is processed first (LIFO).
Push the left child, 6. It is now on top, so the next pop() returns it.
pop() returns 6, printed at once (preorder: root first). 6 is a leaf: no children to push.
pop() returns 7, printed at once (preorder: root first). 7 is a leaf: no children to push.
The stack is empty → the loop ends. Output: 1 2 4 5 3 6 7 — the same as recursive preorder.
public void preorderIterative(TreeNode root) { if (root == null) return; Deque<TreeNode> stack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode temp = stack.pop(); System.out.print(temp.data + " "); if (temp.right != null) stack.push(temp.right); // right first if (temp.left != null) stack.push(temp.left); // left on top } }
Traversals · Step through the code
Inorder Traversal with a Stack
temp starts at the root; the stack is empty.
Push 1 (to be printed later); temp = 1.left = 2. Keep going left.
Push 2 (to be printed later); temp = 2.left = 4. Keep going left.
Push 4 (to be printed later); temp = 4.left = null. Nothing more to the left.
temp is null → pop 4 and print it (everything to its left is done). temp = 4.right = null.
temp is null → pop 2 and print it (everything to its left is done). temp = 2.right = 5.
Push 5 (to be printed later); temp = 5.left = null. Nothing more to the left.
temp is null → pop 5 and print it (everything to its left is done). temp = 5.right = null.
temp is null → pop 1 and print it (everything to its left is done). temp = 1.right = 3.
Push 3 (to be printed later); temp = 3.left = 6. Keep going left.
Push 6 (to be printed later); temp = 6.left = null. Nothing more to the left.
temp is null → pop 6 and print it (everything to its left is done). temp = 6.right = null.
temp is null → pop 3 and print it (everything to its left is done). temp = 3.right = null.
The stack is empty and temp is null → the loop ends. Output: 4 2 5 1 6 3 (left → root → right).
public void inorderIterative(TreeNode root) { if (root == null) return; Deque<TreeNode> stack = new ArrayDeque<>(); TreeNode temp = root; while (!stack.isEmpty() || temp != null) { if (temp != null) { stack.push(temp); temp = temp.left; } else { temp = stack.pop(); System.out.print(temp.data + " "); temp = temp.right; } } }
Traversals
Postorder Traversal with a Stack: Code
public void postorderIterative(TreeNode root) { TreeNode current = root; Deque<TreeNode> stack = new ArrayDeque<>(); while (current != null || !stack.isEmpty()) { if (current != null) { stack.push(current); current = current.left; } else { TreeNode temp = stack.peek().right; if (temp == null) { temp = stack.pop(); System.out.print(temp.data + " "); while (!stack.isEmpty() && temp == stack.peek().right) { temp = stack.pop(); System.out.print(temp.data + " "); } } else { current = temp; } } } }
- A node must be printed only after both of its subtrees, so the node on the stack is not popped right away: we first look at it with
peek. - If the top node has a right child, we go into that subtree first.
- If it has no right child, the node is printed. If the node just printed is the right child of the node now on top, that parent is finished too → it is popped and printed as well.
Traversals · Step through the code
Postorder Traversal with a Stack: Trace
current starts at the root; the stack is empty.
Push 1; current = 1.left = 2.
Push 2; current = 2.left = 4.
Push 4; current = 4.left = null.
peek() = 4 has no right child, so both its subtrees are done: pop and print 4.
4 is not the right child of 2 on top (2.right = 5) → the inner loop stops; 2's right subtree has not been entered yet.
peek() = 2 has a right child, 5, so 2 cannot be printed yet; current = 5 moves into the right subtree first.
Push 5; current = 5.left = null.
peek() = 5 has no right child, so both its subtrees are done: pop and print 5.
The node just printed was the right child of 2, so its right subtree is done too: pop and print 2.
2 is not the right child of 1 on top (1.right = 3) → the inner loop stops; 1's right subtree has not been entered yet.
peek() = 1 has a right child, 3, so 1 cannot be printed yet; current = 3 moves into the right subtree first.
Push 3; current = 3.left = null.
peek() = 3 has a right child, 6, so 3 cannot be printed yet; current = 6 moves into the right subtree first.
Push 6; current = 6.left = null.
peek() = 6 has no right child, so both its subtrees are done: pop and print 6.
The node just printed was the right child of 3, so its right subtree is done too: pop and print 3.
The node just printed was the right child of 1, so its right subtree is done too: pop and print 1.
The stack is empty → the inner loop stops.
current is null and the stack is empty → done. Output: 4 5 2 6 3 1 (left → right → root).
public void postorderIterative(TreeNode root) { TreeNode current = root; Deque<TreeNode> stack = new ArrayDeque<>(); while (current != null || !stack.isEmpty()) { if (current != null) { stack.push(current); current = current.left; } else { TreeNode temp = stack.peek().right; if (temp == null) { temp = stack.pop(); System.out.print(temp.data + " "); while (!stack.isEmpty() && temp == stack.peek().right) { temp = stack.pop(); System.out.print(temp.data + " "); } } else { current = temp; } } } }
Traversals · Step through the code
Level-Order Traversal with a Queue
Add the root to the queue. A queue is FIFO: first in, first out.
poll() returns 4, and prints it.
The left child, 3, joins the back of the queue; it waits behind the nodes ahead of it.
The right child, 7, joins the back.
poll() returns 3, and prints it.
The left child, 5, joins the back of the queue; it waits behind the nodes ahead of it.
The right child, 8, joins the back.
poll() returns 7, and prints it. A leaf: no children to add.
poll() returns 5, and prints it. A leaf: no children to add.
poll() returns 8, and prints it. A leaf: no children to add.
The queue is empty → the loop ends. Output: 4 3 7 5 8 — level by level (preorder would give 4 3 5 8 7).
public void levelOrder(TreeNode root) { if (root == null) return; Queue<TreeNode> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { TreeNode temp = queue.poll(); System.out.print(temp.data + " "); if (temp.left != null) queue.add(temp.left); if (temp.right != null) queue.add(temp.right); } }
Traversals
Application: Expression Trees
- Internal nodes are operators; leaves are operands.
- The same tree gives different notations depending on the traversal.
- The value is computed by a postorder traversal: first the values of both subtrees, then the operator.
| Traversal | Output | Notation |
|---|---|---|
| Preorder | × + 3 4 − 5 2 | prefix |
| Inorder | 3 + 4 × 5 − 2 → with parentheses: (3 + 4) × (5 − 2) | infix |
| Postorder | 3 4 + 5 2 − × → 7 × 3 = 21 | postfix |
The inorder output loses the parentheses; to write the expression correctly, wrap each subtree in parentheses.
Traversals · Step through the code
Evaluating an Expression Tree: evaluate()
evaluate(×): an internal node (operator). Before applying it we need the value of the left operand → evaluate(+).
evaluate(+): an internal node (operator). Before applying it we need the value of the left operand → evaluate(3).
evaluate(3): no children → a leaf, which returns its number: 3.
a = 3. Now the right operand: evaluate(4).
evaluate(4): no children → a leaf, which returns its number: 4.
b = 4. Both subtrees are ready: 3 + 4 = 7 is returned (postorder logic).
a = 7. Now the right operand: evaluate(−).
evaluate(−): an internal node (operator). Before applying it we need the value of the left operand → evaluate(5).
evaluate(5): no children → a leaf, which returns its number: 5.
a = 5. Now the right operand: evaluate(2).
evaluate(2): no children → a leaf, which returns its number: 2.
b = 2. Both subtrees are ready: 5 − 2 = 3 is returned (postorder logic).
b = 3. Both subtrees are ready: 7 × 3 = 21 is returned (postorder logic). (3 + 4) × (5 − 2) = 21
int evaluate(ExprNode node) { if (node.left == null && node.right == null) return node.value; // leaf: operand int a = evaluate(node.left); int b = evaluate(node.right); switch (node.op) { // internal node: operator case '+': return a + b; case '-': return a - b; case '*': return a * b; default: return a / b; } }
Traversals · Step through the code
Finding the Maximum in a Binary Tree
findMax(4): result = 4; now find the maximum of the left subtree.
findMax(3): result = 3; now find the maximum of the left subtree.
findMax(5): result = 5; now find the maximum of the left subtree.
findMax(5.left): empty subtree → returns Integer.MIN_VALUE, which never wins a comparison.
The left result is MIN (left = MIN); now the right subtree.
findMax(5.right): empty subtree → returns Integer.MIN_VALUE, which never wins a comparison.
Comparisons: left = MIN ≤ 5; right = MIN ≤ 5. return 5.
The left result is 5 (left = 5); now the right subtree.
findMax(8): result = 8; now find the maximum of the left subtree.
findMax(8.left): empty subtree → returns Integer.MIN_VALUE, which never wins a comparison.
The left result is MIN (left = MIN); now the right subtree.
findMax(8.right): empty subtree → returns Integer.MIN_VALUE, which never wins a comparison.
Comparisons: left = MIN ≤ 8; right = MIN ≤ 8. return 8.
Comparisons: left = 5 > 3 → result = 5; right = 8 > 5 → result = 8. return 8.
The left result is 8 (left = 8); now the right subtree.
findMax(7): result = 7; now find the maximum of the left subtree.
findMax(7.left): empty subtree → returns Integer.MIN_VALUE, which never wins a comparison.
The left result is MIN (left = MIN); now the right subtree.
findMax(7.right): empty subtree → returns Integer.MIN_VALUE, which never wins a comparison.
Comparisons: left = MIN ≤ 7; right = MIN ≤ 7. return 7.
Comparisons: left = 8 > 4 → result = 8; right = 7 ≤ 8. return 8. Result: 8
public int findMax(TreeNode root) { if (root == null) { return Integer.MIN_VALUE; } int result = root.data; int left = findMax(root.left); int right = findMax(root.right); if (left > result) result = left; if (right > result) result = right; return result; }
Traversals
Recursive Tree Functions
int size(TreeNode node) { // number of nodes if (node == null) return 0; return 1 + size(node.left) + size(node.right); } int height(TreeNode node) { // number of edges if (node == null) return -1; // empty tree: -1 return 1 + Math.max(height(node.left), height(node.right)); } int countLeaves(TreeNode node) { if (node == null) return 0; if (node.left == null && node.right == null) return 1; return countLeaves(node.left) + countLeaves(node.right); }
Traversals · Step through the code
Code Trace: size() — values travel upward
size(9) (the root): not null → first call size(2) on the left subtree.
size(2) (left child of 9): not null → first call size(4) on the left subtree.
size(4) (left child of 2): not null → first call size(null) on the left subtree.
size(4.left) = size(null): empty tree → return 0.
The left result is 0. Now the right subtree: size(null).
size(4.right) = size(null): empty tree → return 0.
size(4) = 1 + 0 + 0 = 1 is returned to the frame of 2.
The left result is 1. Now the right subtree: size(null).
size(2.right) = size(null): empty tree → return 0.
size(2) = 1 + 1 + 0 = 2 is returned to the frame of 9.
The left result is 2. Now the right subtree: size(3).
size(3) (right child of 9): not null → first call size(null) on the left subtree.
size(3.left) = size(null): empty tree → return 0.
The left result is 0. Now the right subtree: size(null).
size(3.right) = size(null): empty tree → return 0.
size(3) = 1 + 0 + 0 = 1 is returned to the frame of 9.
size(9) = 1 + 2 + 1 = 4 is returned. Result: 4
int size(TreeNode node) { // number of nodes if (node == null) return 0; return 1 + size(node.left) + size(node.right); }
Traversals · Step through the code
Code Trace: height()
height(9) (the root): not null → first call height(2) on the left subtree.
height(2) (left child of 9): not null → first call height(4) on the left subtree.
height(4) (left child of 2): not null → first call height(null) on the left subtree.
height(4.left) = height(null): empty tree → return -1. The empty tree has height −1 by convention, so a leaf gets 1 + max(−1, −1) = 0.
The left result is -1. Now the right subtree: height(null).
height(4.right) = height(null): empty tree → return -1.
height(4) = 1 + max(-1, -1) = 0 is returned to the frame of 2.
The left result is 0. Now the right subtree: height(null).
height(2.right) = height(null): empty tree → return -1.
height(2) = 1 + max(0, -1) = 1 is returned to the frame of 9.
The left result is 1. Now the right subtree: height(3).
height(3) (right child of 9): not null → first call height(null) on the left subtree.
height(3.left) = height(null): empty tree → return -1.
The left result is -1. Now the right subtree: height(null).
height(3.right) = height(null): empty tree → return -1.
height(3) = 1 + max(-1, -1) = 0 is returned to the frame of 9.
height(9) = 1 + max(1, 0) = 2 is returned. Result: 2
int height(TreeNode node) { // number of edges if (node == null) return -1; // empty tree: -1 return 1 + Math.max(height(node.left), height(node.right)); }
Traversals · Step through the code
Code Trace: countLeaves()
countLeaves(9) (the root): not a leaf → add up the leaves of both subtrees, left first.
countLeaves(2) (left child of 9): not a leaf → add up the leaves of both subtrees, left first.
countLeaves(4) (left child of 2): both children are null → a leaf; return 1 with no further calls.
countLeaves(2.right) = countLeaves(null): empty tree → return 0.
countLeaves(2) = 1 + 0 = 1 is returned.
countLeaves(3) (right child of 9): both children are null → a leaf; return 1 with no further calls.
countLeaves(9) = 1 + 1 = 2 is returned. Result: 2
int countLeaves(TreeNode node) { if (node == null) return 0; if (node.left == null && node.right == null) return 1; return countLeaves(node.left) + countLeaves(node.right); }
Traversals · Step through the code
Code Trace: Building a Tree from Preorder + Inorder
pre[0] = 1 → new node (the root). In in[], m = 3: [0..2] becomes the left subtree, [4..5] the right subtree.
pre[1] = 2 → new node (left child of 1). In in[], m = 1: [0..0] becomes the left subtree, [2..2] the right subtree.
pre[2] = 4 → new node (left child of 2); m = 0. Both ranges are empty → both calls return null: 4 is a leaf.
pre[3] = 5 → new node (right child of 2); m = 2. Both ranges are empty → both calls return null: 5 is a leaf.
2's subtrees are built → return node(2); 1.left = 2.
pre[4] = 3 → new node (right child of 1). In in[], m = 4: [4..3] becomes the left subtree, [5..5] the right subtree.
The left range [4..3] is empty → build returns null at once: 3 has no left child.
pre[5] = 6 → new node (right child of 3); m = 5. Both ranges are empty → both calls return null: 6 is a leaf.
3's subtrees are built → return node(3); 1.right = 3.
1's subtrees are built → return node(1), which is assigned to root. Result: 1(2(4, 5), 3(—, 6)). The search for m makes this O(n2); keeping the in[] indices in a map makes it O(n).
int p = 0; // next root in pre[] TreeNode build(int[] pre, int[] in, int lo, int hi) { if (lo > hi) return null; // empty range TreeNode node = new TreeNode(pre[p++]); int m = lo; while (in[m] != node.data) m++; node.left = build(pre, in, lo, m - 1); // left part node.right = build(pre, in, m + 1, hi); // right part return node; }
Traversals
Cost of Traversals
| Method | Time | Extra space | Why? |
|---|---|---|---|
| Recursive pre/in/post | O(n) | O(h) | At most h + 2 frames on the call stack (counting the null call) |
| Pre/in/post with a stack | O(n) | O(h) | An explicit stack takes the place of recursion |
| Level order with a queue | O(n) | O(w) | w: the number of nodes on the widest level |
StackOverflowError → prefer the stack-based version.Lab · Step through the Java code
Lab: Traversal with a Stack and a Queue
Summary
Summary
| Topic | Key points |
|---|---|
| Terms | root, parent/child, leaf, internal node; n nodes → n − 1 edges |
| Measures | degree = number of children; depth = edges from the root; height = longest downward path |
| Binary trees | full: 0 or 2 children · complete: last level fills from the left · perfect: every level full |
| Representation | array: index formulas · linked: left/right references |
| Traversal | preorder N-L-R · inorder L-N-R · postorder L-R-N · level order with a queue |
| Cost | every traversal is O(n); extra space O(h) (stack) or O(w) (queue) |
Summary
Interactive Simulators
Summary
Common Mistakes
node.left without first checking node == null throws a NullPointerException. Every recursive tree method starts with the empty tree.StackOverflowError → use the stack- or queue-based version.Summary
Sample Problems
- Write the preorder, inorder, postorder, and level-order outputs of the tree on the right.
- What is the height of the tree? Which nodes are leaves? What is the depth of E?
- Is this tree full, complete, or perfect?
- In a 1-based array representation, at which indices do F and G go? How many cells must the array have at least?
- At most how many nodes can a binary tree of height 4 have?
- Preorder: A B D E C F, inorder: D B E A C F. Draw the tree.
Show answers
- Preorder: A B D E G C F · inorder: D B G E A C F · postorder: D G E B F C A · level order: A B C D E F G
- Height 3 (A→B→E→G); leaves D, G, F; E has depth 2.
- None: E and C each have one child (not full); position 6 is empty while 7 is filled (not complete).
- F: right child of C → 2·3 + 1 = 7; G: left child of E → 2·5 = 10. At least 10 cells (1..10).
- 25 − 1 = 31.
- A is the root; left subtree {D, B, E}: B is its root, D left, E right; right subtree {C, F}: C is its root, F its right child.
Summary
Exercises
int countInternal(TreeNode node): write a method that counts the internal nodes.void mirror(TreeNode node): turn the tree into its mirror image (swap left ↔ right at every node).boolean isSameTree(TreeNode a, TreeNode b): are the two trees identical in shape and values?int countAtDepth(TreeNode node, int d): count the nodes at depth d.- Postorder: D E B F C A, inorder: D B E A F C. Draw the tree.
- How many leaves does a full binary tree with n nodes have? Can n be even?
Answers / hints
nullor a leaf → 0; otherwise 1 + left + right. O(n).- In postorder:
mirror(node.left); mirror(node.right);then swapnode.leftandnode.right. - Both null → true; one null → false; otherwise data equal && lefts equal && rights equal.
d == 0→ 1; otherwisecountAtDepth(node.left, d−1) + countAtDepth(node.right, d−1); null → 0.- The root is A (last); left {D, B, E}: B(D, E); right {F, C}: C, with F as its left child → A(B(D, E), C(F, —)).
- l = i + 1 and n = i + l → l = (n + 1)/2; n = 2i + 1 is always odd.
Summary
References
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.
- Sedgewick and Wayne, Algorithms, 4th ed., 2011.