Sling Academy
Home/Rust/Rust - Modeling adjacency lists for graphs using HashMap<Node, Vec<Node>>

Rust - Modeling adjacency lists for graphs using HashMap>

Last updated: January 04, 2025

Graph data structures are fundamental in computer science, often used to model relationships between objects. A graph consists of nodes (vertices) connected by edges. One common way to represent a graph in memory is through an adjacency list.

An adjacency list represents a graph as a map from each node to a list of nodes that it is connected to. This efficient structure provides O(1) average time complexity for checking the direct connection between two nodes and is well-suited for navigating sparse connections.

In Rust, an efficient way to model an adjacency list is to use a HashMap to represent the graph. Each key in the HashMap is a node, and the corresponding value is a Vec, or vector, that holds the nodes connected to this key node. Let's walk through the step-by-step process of structuring such a graph using Rust's HashMap.

Defining the Data Structure

First, we need to define what a node looks like. For simplicity, let's represent each node with an integer, but in real applications, a node could be a more complex structure with additional data fields.

use std::collections::HashMap;

fn main() {
    // Define the graph as a HashMap
    let mut graph: HashMap<i32, Vec<i32>> = HashMap::new();

    // Add nodes and their connections
    add_edge(&mut graph, 1, 2);
    add_edge(&mut graph, 1, 3);
    add_edge(&mut graph, 2, 4);
    add_edge(&mut graph, 3, 4);

    // Print the graph structure
    for (node, edges) in &graph {
        println!("{}: {:?}", node, edges);
    }
}

fn add_edge(graph: &mut HashMap<i32, Vec<i32>>, start: i32, end: i32) {
    graph.entry(start).or_insert(Vec::new()).push(end);
    graph.entry(end).or_insert(Vec::new()).push(start);
}

In the above implementation, we've created a function add_edge() to add edges between nodes. The or_insert(Vec::new()) method ensures that each node has a vector allocated for its edges if it doesn’t already exist.

Directed Graph

Our example code creates an undirected graph, where each edge is bi-directional. If you wish to represent a directed graph instead, you only need to remove the line that inserts the reverse edge from end to start.

fn add_directed_edge(graph: &mut HashMap<i32, Vec<i32>>, start: i32, end: i32) {
    graph.entry(start).or_insert(Vec::new()).push(end);
}

Traversing the Graph

Traversal allows us to explore the graph structure. A simple approach is depth-first search (DFS) or breadth-first search (BFS).

Depth-First Search (DFS)

Depth-first search explores as far as possible along each branch before backtracking.

fn dfs(node: i32, graph: &HashMap<i32, Vec<i32>>, visited: &mut HashMap<i32, bool>) {
    if visited.get(&node).cloned() != Some(true) {
        println!("Visiting node {}", node);
        visited.insert(node, true);
        if let Some(neighbors) = graph.get(&node) {
            for &neighbor in neighbors {
                dfs(neighbor, graph, visited);
            }
        }
    }
}

fn main() {
    // (initialize graph as before)

    let mut visited: HashMap<i32, bool> = HashMap::new();
    dfs(1, &graph, &mut visited);
}

In depth-first search, we use a HashMap to keep track of visited nodes, ensuring we do not process the same node more than once.

Conclusion

Implementing an adjacency list using HashMap<Node, Vec<Node>> in Rust provides a flexible and efficient way to represent and traverse graphs. This approach is ideal for sparse graphs due to its space-efficient representation and quick adjacency list manipulation.

Next Article: Rust - Designing multi-step transformations from raw input to final structured data using vectors

Previous Article: Rust - Using a vector as a ring buffer or circular data structure

Series: Collections in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior