Introduction

First attempt
- Use objects and methods
- Use an “inner” module named
graph
where classesGraph
(public) andGraphNode
(hidden) are defined (among other things their public API)- This module should be a different file
graph.rs
for example - Here the code must be monolithic so the module in defined/scoped (I don’t know the term) in
main.rs
- This module should be a different file
- About the classes defined
- A
GraphNode
is a value/id and a vector of neighbors - A
Graph
is a vector of GraphNodes - No pointer, nor boxe or whatsoever
- Indeed, at this point the graph is read only (not mutable) so there is no need for
Rc
norRefCell
- Keep in mind that the id of each node is also its value. This helps to keep the code simple
- If this is a problem, rather than :
- A
struct GraphNode {
val: usize,
neighbors: Vec<usize>,
}
One could write
struct GraphNode<T> {
id: usize,
neighbors: Vec<usize>,
data: T,
}
- Pay some attention to
for &neighbor in &self.nodes[node].neighbors {...
- YES : tested on the Rust Playground
mod graph {
use std::collections::{HashSet, VecDeque};
struct GraphNode {
val: usize,
neighbors: Vec<usize>,
}
impl GraphNode {
fn new(val: usize, neighbors: Vec<usize>) -> Self {
Self { val, neighbors }
}
fn process(&self) {
println!("Processing node {}", self.val);
}
}
pub struct Graph {
nodes: Vec<GraphNode>,
}
impl Graph {
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
pub fn from_adjacency_list(adj_list: &[Vec<usize>]) -> Self {
let mut graph = Graph::new();
for (i, neighbors) in adj_list.iter().enumerate() {
graph.nodes.push(GraphNode::new(i, neighbors.clone()));
}
graph
}
pub fn print(&self, node: usize) {
let mut visited = HashSet::new();
self.print_recursive(node, &mut visited);
}
fn print_recursive(&self, node: usize, visited: &mut HashSet<usize>) {
if visited.contains(&node) {
return;
}
visited.insert(node);
println!("Node {} has neighbors {:?}", node, &self.nodes[node].neighbors);
for &neighbor in &self.nodes[node].neighbors {
self.print_recursive(neighbor, visited);
}
}
pub fn dfs(&self, start: usize) {
let mut visited = HashSet::new();
self.dfs_recursive(start, &mut visited);
}
fn dfs_recursive(&self, start: usize, visited: &mut HashSet<usize>) {
if visited.contains(&start) {
return;
}
visited.insert(start);
self.nodes[start].process();
for &neighbor in &self.nodes[start].neighbors {
self.dfs_recursive(neighbor, visited);
}
}
pub fn bfs(&self, start: usize) {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(current) = queue.pop_front() {
if visited.contains(¤t) {
continue;
}
visited.insert(current);
self.nodes[current].process();
for &neighbor in &self.nodes[current].neighbors {
if !visited.contains(&neighbor) {
queue.push_back(neighbor);
}
}
}
}
}
}
use graph::Graph;
fn main() {
// 0
// / | \
// 3 | 1
// \ |
// 2
// /
// 4
let adjacency_list = [
vec![1, 2, 3],
vec![0],
vec![0, 3, 4],
vec![0, 2],
vec![2],
];
let my_graph = Graph::from_adjacency_list(&adjacency_list);
let start_node = 0;
my_graph.print(start_node); // Node 0 has neighbors [1, 2, 3]
// ...
println!("\nDFS from node 0:"); // DFS from node 0:
my_graph.dfs(start_node); // Processing node 0
// ...
println!("\nBFS from node 0:"); // BFS from node 0:
my_graph.bfs(start_node); // Processing node 0
// ...
}
Second attempt
- To stick to the content of the book
dfs()
,bfs()
andprocess()
are not methods but regular functions
- YES : tested on the Rust Playground
use std::collections::{HashSet, VecDeque};
struct GraphNode {
val: usize,
neighbors: Vec<usize>,
}
impl GraphNode {
fn new(val: usize, neighbors: Vec<usize>) -> Self {
Self { val, neighbors }
}
}
struct Graph {
nodes: Vec<GraphNode>,
}
impl Graph {
fn new() -> Self {
Self { nodes: Vec::new() }
}
fn from_adjacency_list(adj_list: &[Vec<usize>]) -> Self {
let mut graph = Graph::new();
for (i, neighbors) in adj_list.iter().enumerate() {
graph.nodes.push(GraphNode::new(i, neighbors.clone()));
}
graph
}
}
fn print_graph(graph: &Graph, start: usize) {
use std::collections::HashSet;
let mut visited = HashSet::new();
// This a way to hide the recursive part to the caller who can only call `print_graph()`
fn deep_copy_recursive_print(graph: &Graph, node: usize, visited: &mut HashSet<usize>) {
if visited.contains(&node) {
return;
}
visited.insert(node);
println!("Node {} has neighbors {:?}", node, graph.nodes[node].neighbors);
for &neighbor in &graph.nodes[node].neighbors {
deep_copy_recursive_print(graph, neighbor, visited);
}
}
deep_copy_recursive_print(graph, start, &mut visited);
}
fn process(node : &GraphNode) {
println!("Processing node {}", node.val);
}
fn dfs(graph: &Graph, start: usize) {
let mut visited = HashSet::new();
fn dfs_recursive(graph: &Graph, start: usize, visited: &mut HashSet<usize>) {
if visited.contains(&start) {
return;
}
visited.insert(start);
process(&graph.nodes[start]);
for &neighbor in &graph.nodes[start].neighbors {
dfs_recursive(graph, neighbor, visited);
}
}
dfs_recursive(graph, start, &mut visited);
}
fn bfs(graph: &Graph, start: usize) {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(current) = queue.pop_front() {
if visited.contains(¤t) {
continue;
}
visited.insert(current);
process(&graph.nodes[current]);
for &neighbor in &graph.nodes[current].neighbors {
if !visited.contains(&neighbor) {
queue.push_back(neighbor);
}
}
}
}
fn main() {
// 0
// / | \
// 3 | 1
// \ |
// 2
// /
// 4
let adjacency_list = [
vec![1, 2, 3],
vec![0],
vec![0, 3, 4],
vec![0, 2],
vec![2],
];
let my_graph = Graph::from_adjacency_list(&adjacency_list);
let start_node = 0;
print_graph(&my_graph, start_node); // Node 0 has neighbors [1, 2, 3]
// ...
println!("\nDFS from node 0:"); // DFS from node 0:
dfs(&my_graph, start_node); // Processing node 0
// ...
println!("\nBFS from node 0:"); // BFS from node 0:
bfs(&my_graph, start_node); // Processing node 0
// ...
}
Third attempt
- In the book,
dfs()
andbfs()
only have a GraphNode as parameter - Here,
my_graph
in encapsulated into aGRAPH
objectGRAPH
is a global static variable whose access is transparently synchronized- In
dfs()
andbfs()
we need tolet graph = GRAPH.get().expect("blablabla...");
before we can act on it
- YES : tested on the Rust Playground
Advantages
- Simplicity: very easy to read, no need for Rc, RefCell, etc.
- No costly dynamic allocation: nodes are contained in a vector, enabling rapid indexing.
- Good read performance: no borrow() overhead or dynamic memory management.
- Ideal for static graphs: if the graph never changes after its creation.
Disadvantages
- Global access (GRAPH): OnceLock imposes a single graph, so :
- no parallel tests,
- no simultaneous multi-graphs,
- no dynamic reloading.
- Rigid index: less flexible for more complex algos (e.g. node deletion, transformation).
- Not usable in multithreaded environments without explicit locks: everything is fixed at the OnceLock level, but internal accesses are not protected.
- No granularity on nodes: impossible to access a node independently of the graph.
use std::collections::{HashSet, VecDeque};
use std::sync::OnceLock;
#[derive(Debug)]
struct GraphNode {
val: usize,
neighbors: Vec<usize>,
}
impl GraphNode {
fn new(val: usize, neighbors: Vec<usize>) -> Self {
Self { val, neighbors }
}
}
#[derive(Debug)]
struct Graph {
nodes: Vec<GraphNode>,
}
impl Graph {
fn new() -> Self {
Self { nodes: Vec::new() }
}
fn from_adjacency_list(adj_list: &[Vec<usize>]) -> Self {
let mut graph = Graph::new();
for (i, neighbors) in adj_list.iter().enumerate() {
graph.nodes.push(GraphNode::new(i, neighbors.clone()));
}
graph
}
}
// Global lazy static for the graph
static GRAPH: OnceLock<Graph> = OnceLock::new();
fn print_graph(start: usize) {
use std::collections::HashSet;
let mut visited = HashSet::new();
let graph = GRAPH.get().expect("Graph not initialized");
// This a way to hide the recursive part to the caller who can only call `print_graph()`
fn deep_copy_recursive_print(graph: &Graph, node: usize, visited: &mut HashSet<usize>) {
if visited.contains(&node) {
return;
}
visited.insert(node);
println!("Node {} has neighbors {:?}", node, graph.nodes[node].neighbors);
for &neighbor in &graph.nodes[node].neighbors {
deep_copy_recursive_print(graph, neighbor, visited);
}
}
deep_copy_recursive_print(graph, start, &mut visited);
}
// Function to process a node
fn process(node: &GraphNode) {
println!("Processing node {}", node.val);
}
// DFS wrapper using global graph
fn dfs(start: usize) {
let mut visited = HashSet::new();
let graph = GRAPH.get().expect("Graph not initialized");
dfs_recursive(graph, start, &mut visited);
}
// Recursive DFS helper
fn dfs_recursive(graph: &Graph, start: usize, visited: &mut HashSet<usize>) {
if visited.contains(&start) {
return;
}
visited.insert(start);
process(&graph.nodes[start]);
for &neighbor in &graph.nodes[start].neighbors {
dfs_recursive(graph, neighbor, visited);
}
}
// BFS using global graph
fn bfs(start: usize) {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
let graph = GRAPH.get().expect("Graph not initialized");
while let Some(current) = queue.pop_front() {
if visited.contains(¤t) {
continue;
}
visited.insert(current);
process(&graph.nodes[current]);
for &neighbor in &graph.nodes[current].neighbors {
if !visited.contains(&neighbor) {
queue.push_back(neighbor);
}
}
}
}
fn main() {
let adjacency_list = [
vec![1, 2, 3],
vec![0],
vec![0, 3, 4],
vec![0, 2],
vec![2],
];
let my_graph = Graph::from_adjacency_list(&adjacency_list);
let start_node = 0;
GRAPH.set(my_graph).expect("GRAPH already initialized");
print_graph(start_node); // Node 0 has neighbors [1, 2, 3]
// ...
println!("\nDFS from node 0:"); // DFS from node 0:
dfs(start_node); // Processing node 0
// ...
println!("\nBFS from node 0:"); // BFS from node 0:
bfs(start_node); // Processing node 0
// ...
}
Fourth Attempt
- Mimic the book as much as possible
- There are no more
Graph
classes at all, just a fewGraphNodes
. - A
GraphNode
is a value and a list of pointers to other node (GraphNodeRef
) Rc
andRefCell
are mandatory- We need pointers pointing to the same nodes =>
Rc
RefCell
supports interior mutability. This is a way of bypassing Rust’s borrowing rules at runtime rather than at compile-time.- Without
RefCell
, anRc<Node>
cannot be modified. Rust forbids to modify a structure contained in an Rc. An Rc only gives (immutable) read access to the underlying data. See :.borrow()
and.borrow_mut()
- We need pointers pointing to the same nodes =>
- There is a cost at runtime.
- Preferred solution?
- YES : tested on the Rust Playground
Advantages
- Modularity per node: each node is an independent entity (strong pointer + internal mutability).
- Easy to modify dynamically: neighbors can be added/removed without rebuilding everything.
- No need for central structure: more flexible for building multiple graphs, sub-graphs, etc.
- Allows cycles. Useful for undirected or cyclic graphs.
Disadvantages
- Management complexity (
Rc
,RefCell
): need toborrow()
andclone()
at each access, potential source of panic (borrow_mut() in conflict). - Not thread-safe:
Rc
andRefCell
are not Send or Sync. Unusable as is in multithreaded mode without conversion toArc<Mutex<...>>
. - Memory overhead: each node has its own reference counter and dynamic allocation.
use std::cell::RefCell;
use std::collections::{HashSet, VecDeque};
use std::rc::Rc;
// Type alias for readability
type GraphNodeRef = Rc<RefCell<GraphNode>>;
// Definition of a graph node
#[derive(Debug)]
struct GraphNode {
val: usize,
neighbors: Vec<GraphNodeRef>,
}
// Helper to build graph from adjacency list
fn from_adjacency_list(adj_list: &[Vec<usize>]) -> Vec<GraphNodeRef> {
let nodes: Vec<GraphNodeRef> = (0..adj_list.len()).map(|i| Rc::new(RefCell::new(GraphNode { val: i, neighbors: vec![] }))).collect();
for (i, neighbors) in adj_list.iter().enumerate() {
let mut node_mut = nodes[i].borrow_mut();
node_mut.neighbors = neighbors.iter().map(|&j| Rc::clone(&nodes[j])).collect();
}
nodes
}
fn print_graph(node: &GraphNodeRef) {
use std::collections::HashSet;
let mut visited = HashSet::new();
// This a way to "hide" the recursive part to the caller who can only call `print_graph()`
fn deep_copy_recursive_print(node: &GraphNodeRef, visited: &mut HashSet<usize>) {
let val = node.borrow().val;
if visited.contains(&val) {
return;
}
visited.insert(val);
let neighbors: Vec<_> = node.borrow().neighbors.iter().map(|n| n.borrow().val).collect();
println!("Node {} has neighbors {:?}", val, neighbors);
for neighbor in &node.borrow().neighbors {
deep_copy_recursive_print(neighbor, visited);
}
}
deep_copy_recursive_print(node, &mut visited);
}
fn process(node: &GraphNodeRef) {
println!("Processing node {}", node.borrow().val);
}
fn dfs(node: &GraphNodeRef) {
let mut visited = HashSet::new();
// Hide the recursive part
fn dfs_recursive(node: &GraphNodeRef, visited: &mut HashSet<usize>) {
match visited.insert(node.borrow().val) {
false => (), // Already visited
true => {
process(node);
for neighbor in &node.borrow().neighbors {
dfs_recursive(neighbor, visited);
}
}
}
}
dfs_recursive(node, &mut visited);
}
fn bfs(node: &GraphNodeRef) {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(Rc::clone(node));
while let Some(current) = queue.pop_front() {
let val = current.borrow().val;
if visited.contains(&val) {
continue;
}
visited.insert(val);
process(¤t); // Visit current node
for neighbor in ¤t.borrow().neighbors {
if !visited.contains(&neighbor.borrow().val) {
queue.push_back(Rc::clone(neighbor)); // Push unvisited neighbor
}
}
}
}
fn main() {
// 0
// / | \
// 3 | 1
// \ |
// 2
// /
// 4
let adjacency_list = [
vec![1, 2, 3],
vec![0],
vec![0, 3, 4],
vec![0, 2],
vec![2],
];
let my_graph = from_adjacency_list(&adjacency_list);
let start_node = &my_graph[0];
print_graph(start_node); // Node 0 has neighbors [1, 2, 3]
// ...
println!("\nDFS from node 0:"); // DFS from node 0:
dfs(start_node); // Processing node 0
// ...
println!("\nBFS from node 0:"); // BFS from node 0:
bfs(start_node); // Processing node 0
// ...
}
Same as above but only demonstrate the call to dfs()
- YES : tested on the Rust Playground
use std::cell::RefCell;
use std::collections::HashSet;
use std::rc::Rc;
// Type alias for readability
type GraphNodeRef = Rc<RefCell<GraphNode>>;
#[derive(Debug)]
struct GraphNode {
val: usize,
neighbors: Vec<GraphNodeRef>,
}
fn from_adjacency_list(adj_list: &[Vec<usize>]) -> Vec<GraphNodeRef> {
let nodes: Vec<GraphNodeRef> = (0..adj_list.len()).map(|i| Rc::new(RefCell::new(GraphNode { val: i, neighbors: vec![] }))).collect();
for (i, neighbors) in adj_list.iter().enumerate() {
let mut node_mut = nodes[i].borrow_mut();
node_mut.neighbors = neighbors.iter().map(|&j| Rc::clone(&nodes[j])).collect();
}
nodes
}
fn process(node: &GraphNodeRef) {
println!("Processing node {}", node.borrow().val);
}
fn dfs(node: &GraphNodeRef) {
let mut visited = HashSet::new();
fn dfs_recursive(node: &GraphNodeRef, visited: &mut HashSet<usize>) {
match visited.insert(node.borrow().val) {
false => (), // Already visited
true => {
process(node);
for neighbor in &node.borrow().neighbors {
dfs_recursive(neighbor, visited);
}
}
}
}
dfs_recursive(node, &mut visited);
}
fn main() {
let adjacency_list = [
vec![1, 2, 3],
vec![0],
vec![0, 3, 4],
vec![0, 2],
vec![2],
];
let my_graph = from_adjacency_list(&adjacency_list);
let start_node = &my_graph[0];
println!("\nDFS from node 0:"); // DFS from node 0:
dfs(start_node); // Processing node 0
// ...
}