Recursion and the call stack
A recursive function calls itself on a smaller subproblem. A base case returns without another call. For factorial, fact(0)=1 and fact(n)=n×fact(n−1) for nonnegative n. Each pending call keeps local state on the call stack; returning unwinds those frames. Show that a nonnegative measure decreases, otherwise a base case alone does not guarantee termination. Deep recursion also consumes stack memory; Python limits depth, so an iterative solution or explicit stack may be preferable for large inputs.
Why is fact(n+1) instead of fact(n−1) a problem?
Worked solution
The argument moves away from zero, so the base case is not reached.
Trees and traversals
A rooted tree has a distinguished root and parent-child structure without cycles. A binary tree has at most two children per node. Preorder visits node, left, right; inorder visits left, node, right; postorder visits left, right, node. In a binary search tree, all left keys precede the node and right keys follow it, so inorder yields sorted keys. Height controls search cost: balanced trees have logarithmic height, while a chain can have linear height. The lab constructs immutable nested tuples rather than a full mutable search-tree implementation.
Does inorder sort every arbitrary binary tree?
Worked solution
No, only trees satisfying the search-order property.
Graphs and representation
A graph has vertices and edges. Edges may be directed, undirected or weighted. An adjacency list stores neighbours for each vertex, using O(V+E) storage in the usual model; an adjacency matrix uses O(V²) and supports direct edge checks. Unlike a tree, a graph can have cycles and multiple paths, so traversal needs visited state. Mark a vertex when scheduling it to avoid adding the same vertex repeatedly. Specify whether isolated vertices, unknown endpoints and parallel edges are allowed.
Why mark visited on enqueue rather than only on removal?
Worked solution
To prevent multiple predecessors from scheduling the same vertex.
DFS and BFS
Depth-first search follows a branch before alternatives, using recursion or an explicit stack. Breadth-first search uses a queue and explores vertices in nondecreasing edge distance from the start. With adjacency lists, both traverse reachable vertices and edges in O(V+E), assuming constant-time visited checks. BFS stores a predecessor when first discovering each vertex. Following predecessors backwards reconstructs a route. DFS finds a route but does not generally minimise its edge count.
Which frontier structure gives BFS its order?
Worked solution
A FIFO queue.
Weighted paths and limits
BFS minimises edges only when each edge has the same cost. For nonnegative varying weights, Dijkstra repeatedly finalises the vertex with the smallest tentative distance and relaxes outgoing edges. A priority queue supports efficient selection. A direct edge of cost 100 loses to two edges costing 1 each, despite having fewer edges. Negative weights violate Dijkstra's finalisation argument; other algorithms are needed. State the objective before calling a route shortest, and distinguish unreachable from unknown vertices.
Does BFS minimise travel time when road times vary?
Worked solution
No, it minimises edge count, not varying total time.
Common misconceptions
- The returned-list tree lab is illustrative; repeated concatenation is not an optimal traversal.
- A shortest route must specify its cost model.
Lab setup
Download each script and run it in a terminal with Python 3.11 or later: python m07_tree.py. On Windows, py -3 is an alternative; on some systems use python3. The labs use only the standard library. Predict the result before running, then complete the variations. Run without -O so assertions remain enabled. Outputs below were captured by the builder. Code and output are identical in both language editions.
Lab 1 — Recursive trees
Draw the tree, predict traversal and height, then follow the base case for None.
"""Recursive traversal with an explicit empty-tree base case."""
def inorder(node):
if node is None:
return []
value, left, right = node
return inorder(left) + [value] + inorder(right)
def height(node):
if node is None:
return 0
return 1 + max(height(node[1]), height(node[2]))
tree = (4, (2, (1, None, None), (3, None, None)), (6, None, None))
print("inorder:", inorder(tree))
print("height:", height(tree))
assert inorder(tree) == [1, 2, 3, 4, 6]
assert inorder(None) == [] and height(None) == 0
inorder: [1, 2, 3, 4, 6]
height: 3
- Implement preorder.
- Make a three-node chain.
- Explain why concatenating lists repeatedly adds copying cost.
Worked solution
Preorder is [value]+preorder(left)+preorder(right). A three-node chain has height 3. The returned-list implementation copies partial outputs repeatedly and can be quadratic on a skewed tree; an accumulator traversal appending each item once avoids that.
Lab 2 — A route finder
The graph includes cycles and an isolated vertex. BFS records parents at discovery and returns None for an unreachable target.
"""BFS finds a shortest path measured in edges in an unweighted graph."""
from collections import deque
def route(graph, start, goal):
if start not in graph or goal not in graph:
raise KeyError("unknown vertex")
queue, parent = deque([start]), {start: None}
while queue:
vertex = queue.popleft()
if vertex == goal:
path = []
while vertex is not None:
path.append(vertex); vertex = parent[vertex]
return path[::-1]
for neighbour in graph[vertex]:
if neighbour not in parent:
parent[neighbour] = vertex; queue.append(neighbour)
return None
graph = {"A": ["B", "C"], "B": ["A", "D"], "C": ["A", "D"], "D": ["B", "C"], "E": []}
print("A to D:", route(graph, "A", "D"))
print("A to E:", route(graph, "A", "E"))
assert route(graph, "A", "D") == ["A", "B", "D"]
assert route(graph, "A", "A") == ["A"]
assert route(graph, "A", "E") is None
A to D: ['A', 'B', 'D']
A to E: None
- Swap A's neighbour order.
- Add E to D's neighbours in both directions.
- Pass an unknown vertex and compare with unreachable E.
Worked solution
The alternate equally short route is A,C,D. Connecting E gives A,B,D,E. Unknown vertices raise KeyError; valid but disconnected vertices return None. Ties depend on neighbour order, not on a different distance.
Exercises with worked solutions
Try before opening the solution. ★ applies an idea; ★★ combines ideas; ★★★ asks for design or proof.
Give the base case for counting nodes in an empty tree.
Worked solution
Return 0 for None; otherwise 1+count(left)+count(right).
For root B with children A,C, give inorder and preorder.
Worked solution
Inorder A,B,C; preorder B,A,C.
Why can DFS loop forever on A→B→A without visited state?
Worked solution
Each recursive step revisits the other vertex, with no decreasing unvisited set or exit. Mark visits before exploring neighbours.
Which representation is smaller for a sparse graph?
Worked solution
An adjacency list, O(V+E), rather than an O(V²) matrix when E is much smaller than V².
A→C costs 10; A→B→C costs 2+2. Which does BFS favour?
Worked solution
Direct A→C has one edge; it is not minimum cost. Weighted shortest path should select total cost 4.
Why is a first-discovered BFS path shortest in edges?
Worked solution
The queue processes all vertices at distance d before d+1. New neighbours get distance d+1; any shorter predecessor would already have been processed, so a shorter discovery cannot arrive later.
Compare recursive traversal stack depth in balanced and chain trees.
Worked solution
Depth equals height: O(log n) for a balanced tree, O(n) for a chain. Both still visit n nodes.
Why reverse the accumulated parent path?
Worked solution
Following parents starts at goal and ends at start; reversing gives the requested start-to-goal order.
Self-check quiz
Choose an answer for feedback; reset to retry. A text answer key is available without JavaScript.
Recursion needs?
BFS uses?
BFS minimises?
Dijkstra requires?
A graph traversal needs visited state because?
Inorder yields sorted keys when?
Answer key
- A — Progress makes the base reachable.
- B — FIFO expands layers.
- C — Equal-cost edges make edge distance meaningful.
- A — Negative weights break finalisation.
- B — It prevents repeated scheduling.
- C — The left/right key invariant is necessary.
Guided reading
- MIT algorithm lectures — Read graph search and shortest paths; distinguish the objectives.
Review and the next step
Trace parents for an unreachable and a tied shortest path. Explain the recursive empty case. Module 08 compares design strategies beyond traversal.
Key terms
| Term | Meaning |
|---|---|
| Frontier | Discovered vertices awaiting processing. |
| Relaxation | Improving a tentative distance via an edge. |
| Height | Here, the maximum number of nodes on a root-to-leaf path. |