Course contentsShow
← Rust
Lesson 390 of 1,9104. Lifetimes and Advanced OwnershipFree lesson

Graph Construction via Indexing

Replace pointer-based graphs with index-based structures to bypass borrow checker limitations.

In this lesson, you will learn how to represent cyclic data structures—like graphs—by replacing direct memory pointers with stable numeric indices to bypass the borrow checker’s rigid ownership rules.

The Problem with Pointers

In many languages, a graph node holds a pointer to its neighbor (&Node). In Rust, if you try to create a graph where nodes point to each other, the borrow checker will fight you. Because each node "owns" or borrows the next, you quickly run into issues with self-referential structures or cycles that violate the "one mutable reference or many immutable references" rule. Even if you use Rc<RefCell<Node>>, you can easily create memory leaks and complex overhead.

The Indexing Solution

Instead of storing direct references, we store all our nodes in a central Vec<Node> and represent connections as usize indices. The Vec acts as an "Arena," serving as the single source of truth for the graph's memory.

struct Node {
    value: i32,
    neighbors: Vec<usize>, // Store indices, not references
}

struct Graph {
    nodes: Vec<Node>,
}

impl Graph {
    fn add_edge(&mut self, from: usize, to: usize) {
        self.nodes[from].neighbors.push(to);
    }
}

Why This Works

  1. Ownership: The Graph struct owns the Vec, and the Vec owns the Nodes. There are no shared, mutable references to manage, so the borrow checker is happy.
  2. Safety: Indices are just integers. You can copy them, store them, and move them around without worrying about lifetimes or invalidating pointers.
  3. Efficiency: Vectors provide cache-friendly, contiguous memory layout. By using indices, you avoid the fragmentation associated with many small, heap-allocated objects connected by pointers.

When you need to traverse the graph, you simply pass the index to the Graph struct: let node = &graph.nodes[index];. This keeps your logic clean and separates the structure of your data from the memory management of your data. This pattern effectively decouples the lifetime of a node from the lifetime of its neighbors, allowing you to build complex, cyclic relationships without fighting the compiler.

Key Takeaway: By storing nodes in a central arena and using indices instead of references, you turn a complex ownership problem into a simple array lookup, effectively sidestepping borrow checker restrictions.