Lecture 36 - Graph representation & traversals
Logistics
- HW7 due Friday Dec 5 (no corrections)
- HW6 still being graded
- Last three lectures: Graph algorithms
Learning objectives
By the end of today, you should be able to:
- Represent graphs using adjacency lists and adjacency matrices
- Explain BFS and DFS algorithms
- Analyze the time and space complexity of graph algorithms
- Recognize applications of BFS and DFS
What is a graph?
Graph: A collection of nodes (vertices) connected by edges
Unlike trees:
- No root
- Can have cycles
- Can have multiple paths between nodes
- Edges can be directed OR undirected
Graph terminology
Basic terms:
A --- B
| |
C --- D --- E
- Vertex/Node: A, B, C, D, E (the circles)
- Edge: Connection between vertices (the lines)
- Neighbors/Adjacent: B is adjacent to A and D
- Degree: Number of edges connected to vertex
- Degree of A = 2 (connected to B and C)
- Degree of D = 3 (connected to B, C, E)
- Path: Sequence of vertices connected by edges
- A → B → D → E is a path from A to E
- Cycle: Path that starts and ends at same vertex
- A → B → D → C → A is a cycle
Directed vs Undirected graphs
Undirected graph: Edges have no direction (two-way streets)
A --- B A can reach B, B can reach A
| |
C --- D
Directed graph (digraph): Edges have direction (one-way streets)
A --> B A can reach B, but B cannot reach A
^ |
| v
C <-- D C and D have two edges, one going each way
-->
Example: Twitter follows are directed (you can follow someone who doesn't follow back)
(If you're in 122, you've been seeing a lot of these with Markov Chains!)
When there are no cycles (paths back to a node once you leave) we call this a Directed Acyclic Graph or DAG
Weighted vs Unweighted graphs
Unweighted: All edges are equal
A --- B
Weighted: Edges have costs/weights
A --5-- B Distance, cost, time, etc.
Example: Road network with distances, flight routes with costs, Markov Chains with probabilities
Think-pair-share: Graph examples
Which of these are naturally modeled as
- Weighted/unweighted
- Directed/undirected
- Cyclic/acyclic
- Social network (Facebook, LinkedIn)
- Road map
- Course prerequisites
- Web pages with links
- Family tree
- Chess game states
Challenge: How to store a graph?
Need to answer:
- What vertices exist?
- Which vertices are connected?
- (For weighted graphs) What are the edge weights?
Two main approaches:
- Adjacency List
- Adjacency Matrix
Adjacency list
Idea: For each vertex, store a list of its neighbors
Example graph:
0 --- 1
| |
2 --- 3
Adjacency list representation:
0: [1, 2]
1: [0, 3]
2: [0, 3]
3: [1, 2]
In Rust (using Vec of Vecs):
fn main() { // Graph with 4 vertices (0, 1, 2, 3) let graph: Vec<Vec<usize>> = vec![ vec![1, 2], vec![0, 3], vec![0, 3], vec![1, 2], ]; // Check if edge exists: 0 -- 1? if graph[0].contains(&1) { println!("Edge 0-1 exists"); } // Iterate over neighbors of vertex 2 for &neighbor in &graph[2] { println!("2 is connected to {}", neighbor); } }
With HashMap (when vertices aren't 0..n):
use std::collections::HashMap; fn main() { let mut graph: HashMap<&str, Vec<&str>> = HashMap::new(); graph.insert("Alice", vec!["Bob", "Charlie"]); graph.insert("Bob", vec!["Alice", "David"]); graph.insert("Charlie", vec!["Alice", "David"]); graph.insert("David", vec!["Bob", "Charlie"]); // Neighbors of Alice if let Some(neighbors) = graph.get("Alice") { println!("Alice's friends: {:?}", neighbors); } }
Adjacency matrix
Idea: 2D array where matrix[i][j] = 1 if edge from i to j exists
(Yep, we're in "row-stochastic world" here... sorry)
Example graph (same as before):
0 --- 1
| |
2 --- 3
Adjacency matrix:
0 1 2 3
0 [0, 1, 1, 0] Row 0: edges from vertex 0
1 [1, 0, 0, 1] Row 1: edges from vertex 1
2 [1, 0, 0, 1] Row 2: edges from vertex 2
3 [0, 1, 1, 0] Row 3: edges from vertex 3
In Rust:
fn main() { // Graph with 4 vertices let graph: Vec<Vec<usize>> = vec![ vec![0, 1, 1, 0], vec![1, 0, 0, 1], vec![1, 0, 0, 1], vec![0, 1, 1, 0], ]; // Check if edge exists: 0 -- 1? if graph[0][1] == 1 { println!("Edge 0-1 exists"); } // Find all neighbors of vertex 2 for j in 0..graph[2].len() { if graph[2][j] == 1 { println!("2 is connected to {}", j); } } }
For weighted graphs: Store weight instead of 1, use 0 or ∞ for no edge
Adjacency list vs adjacency matrix
Graph with V vertices, E edges:
| Operation | Adjacency List | Adjacency Matrix |
|---|---|---|
| Space | O(V + E) | O(V^2) |
| Check if edge (u,v) exists | O(degree of u) | O(1) |
| Find all neighbors of u | O(degree of u) | O(V) |
| Add edge | O(1) | O(1) |
| Remove edge | O(degree of u) | O(1) |
Rule of thumb: Use adjacency list unless you have a good reason not to!
They are especially useful for sparse graphs - and most real-world graphs are sparse!
The challenge of exploring a graph
Given a graph and starting vertex, we want to visit all reachable vertices
Two main strategies:
- BFS: Explore level by level (breadth-first)
- DFS: Explore as far as possible, then backtrack (depth-first)
BFS vs DFS in ten seconds: https://www.youtube.com/shorts/L1vGm2_cPU0
BFS: The idea
Breadth-First Search: Explore vertices in order of their distance from start
- Like ripples in a pond
- Or the weird image that works for me, like pinching the graph at a point and "picking it up" and letting the rest fall down by gravity...
Example graph (before "picking up"):
E---C
\ \
\ A---B
\ / /
D---/
\
F
"Picked up" by A:
A
/ \
B C
\ / \
D E
\
F
BFS traversal starting at A:
Level 0: A
Level 1: B, C (neighbors of A)
Level 2: D, E (neighbors of B and C)
Level 3: F (neighbor of E)
BFS Algorithm
High-level:
- Start with source vertex in a queue
- Mark source as visited
- While queue not empty:
- Pop a vertex
- For each unvisited neighbor:
- Mark as visited
- Push onto the queue
Notice that: Queue ensures we explore level by level
BFS Example
Graph:
0 --- 1 --- 4
| |
2 --- 3
BFS from vertex 0:
Step 1: Queue = [0], Visited = {0}
Pop 0, Push neighbors 1, 2
Queue = [1, 2], Visited = {0, 1, 2}
Step 2: Pop 1, Push unvisited neighbors 3, 4
Queue = [2, 3, 4], Visited = {0, 1, 2, 3, 4}
Step 3: Pop 2, no new neighbors (0 and 3 already visited)
Queue = [3, 4], Visited = {0, 1, 2, 3, 4}
Step 4: Pop 3, no new neighbors
Queue = [4], Visited = {0, 1, 2, 3, 4}
Step 5: Pop 4, no new neighbors
Queue = [], Done!
Order visited: 0, 1, 2, 3, 4
BFS Implementation in Rust
#![allow(unused)] fn main() { use std::collections::{VecDeque, HashSet}; fn bfs(graph: &Vec<Vec<usize>>, start: usize) { let mut queue = VecDeque::new(); let mut visited = HashSet::new(); queue.push_back(start); visited.insert(start); while let Some(vertex) = queue.pop_front() { println!("Visiting: {}", vertex); for &neighbor in &graph[vertex] { if !visited.contains(&neighbor) { visited.insert(neighbor); queue.push_back(neighbor); } } } } }
Output: Visiting: 0, 1, 2, 3, 4
BFS Applications
1. Shortest path in unweighted graph
- BFS finds shortest path from source to all vertices!
- Distance = level in BFS tree
2. Connected components
- Run BFS from each unvisited vertex
- Each BFS finds one connected component
3. Bipartite testing
- Can graph be 2-colored? (vertices colored so no edge connects same color)
- Use BFS to assign colors
4. Social networks
- Find degrees of separation (Kevin Bacon number, Erdős number, Erdős-Bacon number...)
BFS Complexity
Time complexity:
- Visit each vertex once: O(V)
- Check each edge at most twice (once from each endpoint): O(E)
- Total: O(V + E)
Space complexity:
- Queue: O(V) in worst case
- Visited set: O(V)
- Total: O(V)
Overall linear in graph size
Depth-first search (DFS)
Depth-First Search: Explore as far as possible along each branch before backtracking
Like maze exploration - keep going until you hit a dead end, then backtrack
Example graph:
A
/ \
B C
| |
D F
|
E
Use a stack for this one! Explore deeply before exploring breadth
DFS vs BFS
BFS (Queue - FIFO):
0
/ \
1 2
/ \
3 4
Order: 0, 1, 2, 3, 4 (level by level)
DFS (Stack/Recursion - LIFO):
0
/ \
1 2
/ \
3 4
Order: 0, 1, 3, 4, 2 (go deep first)
DFS Algorithm
High-level
- Start with source vertex in a stack
- While stack not empty:
- Pop a vertex
- Mark as visited
- For each unvisited neighbor:
- Push onto the queue
Notice two differences from BFS:
- Stack instead of queue
- Mark as visited after pop instead of after push
DFS Example
Graph:
0 --- 1 --- 4
| |
2 --- 3
DFS from vertex 0:
Step 1: Stack = [0], Visited = {}
Pop 0, mark as visited, push neighbors 2, 1
Stack = [2, 1], Visited = {0}
Step 2: Pop 1, mark as visited, push unvisited neighbors 4, 3
Stack = [2, 4, 3], Visited = {0, 1}
Step 3: Pop 3, mark as visited, push unvisited neighbors 2
Stack = [2, 4, 2], Visited = {0, 1, 3}
Step 4: Pop 2, mark as visited (no new neighbors - 0 and 3 already visited)
Stack = [2, 4], Visited = {0, 1, 2, 3}
Step 5: Pop 2, already visited, skip
Stack = [4], Visited = {0, 1, 2, 3}
Step 6: Pop 4, mark as visited, no new neighbors
Stack = [], Done!
Order visited: 0, 1, 3, 2, 4
Different from BFS order!
DFS Implementation
#![allow(unused)] fn main() { use std::collections::{HashSet}; fn dfs_iterative(graph: &Vec<Vec<usize>>, start: usize) { let mut stack = vec![start]; let mut visited = HashSet::new(); while !stack.is_empty() { let vertex = stack.pop().unwrap(); if visited.contains(&vertex) { continue; } visited.insert(vertex); println!("Visiting: {}", vertex); for &neighbor in &graph[vertex] { if !visited.contains(&neighbor) { stack.push(neighbor); } } } } }
Note: Order might differ slightly from recursive version depending on how neighbors are added
DFS Applications
1. Pathfinding
- Find any path between two vertices
- Not necessarily shortest (unlike BFS)
2. Cycle detection
- If we encounter a visited vertex that's not the parent, there's a cycle
3. Topological sorting (next lecture!)
- Order vertices in directed acyclic graph
4. Solving puzzles
- Sudoku, N-queens (try solutions, backtrack if invalid)
DFS Complexity
Time complexity:
- Visit each vertex once: O(V)
- Explore each edge at most twice: O(E)
- Total: O(V + E)
Space complexity (recursive):
- Recursion stack: O(V) in worst case (if graph is a long chain)
- Visited set: O(V)
- Total: O(V)
Same as BFS!
Think about: BFS vs DFS
When to use BFS:
- Find shortest path (unweighted)
- Find closest/nearest items
- Level-order traversal
When to use DFS:
- Explore all paths
- Detect cycles
- Topological sort
- Solve mazes/puzzles (with backtracking)
Both work for: Connected components, reachability
Summary
BFS (Queue): DFS (Stack):
0 0
/ \ / \
1 2 1 2
/ \ / \
3 4 3 4
Visit: 0,1,2,3,4 Visit: 0,1,3,4,2
(breadth-first) (depth-first)
| Property | BFS | DFS |
|---|---|---|
| Data structure | Queue | Stack/Recursion |
| Time | O(V + E) | O(V + E) |
| Space | O(V) | O(V) |
| Shortest path | Yes | No |
| Memory | More (queue can be large) | Less (stack depth = path length) |
| Applications | Shortest path, level-order | Cycle detection, topological sort |
Activity time
See website / gradescope