O grafo é uma das estruturas de dados mais versáteis da computação. Ele modela relações entre entidades: redes sociais, mapas de rotas, dependências de pacotes, fluxo de rede e muito mais. A lista de adjacência é a representação mais comum e eficiente para grafos esparsos (onde o número de arestas é muito menor que o total possível).
Conceito e Teoria
O que é um Grafo?
Um grafo G = (V, E) consiste em:
- V (vértices): o conjunto de nós
- E (arestas): o conjunto de conexões entre nós
Grafo Não-Direcionado: Grafo Direcionado (Digrafo):
0 ─── 1 0 ──→ 1
│ / │ │ ↗ │
│ / │ │ / │
│ / │ ↓ / ↓
2 ─── 3 2 ──→ 3
Aresta (0,1) = aresta (1,0) Arco (0,1) ≠ arco (1,0)
Lista de Adjacência
Para cada vértice, mantemos uma lista dos seus vizinhos:
Grafo:
0 ─── 1
│ / │
│ / │
│ / │
2 ─── 3
Lista de Adjacência:
┌─────┬──────────────┐
│ 0 │ [1, 2] │
├─────┼──────────────┤
│ 1 │ [0, 2, 3] │
├─────┼──────────────┤
│ 2 │ [0, 1, 3] │
├─────┼──────────────┤
│ 3 │ [1, 2] │
└─────┴──────────────┘
Em Rust: Vec<Vec<usize>>
adj[0] = vec![1, 2]
adj[1] = vec![0, 2, 3]
adj[2] = vec![0, 1, 3]
adj[3] = vec![1, 2]
Grafo Direcionado
0 ──→ 1
│ ↘
↓ 3
2 ──→ ↗
Lista de Adjacência (apenas saídas):
┌─────┬──────────┐
│ 0 │ [1, 2] │ 0 aponta para 1 e 2
├─────┼──────────┤
│ 1 │ [3] │ 1 aponta para 3
├─────┼──────────┤
│ 2 │ [3] │ 2 aponta para 3
├─────┼──────────┤
│ 3 │ [] │ 3 não aponta para ninguém
└─────┴──────────┘
Grafo Ponderado
Cada aresta tem um peso (custo/distância):
0 ──5── 1
│ / │
3 2 7
│ / │
2 ──4── 3
Lista de Adjacência com pesos:
┌─────┬────────────────────────┐
│ 0 │ [(1, 5), (2, 3)] │
├─────┼────────────────────────┤
│ 1 │ [(0, 5), (2, 2), (3, 7)]│
├─────┼────────────────────────┤
│ 2 │ [(0, 3), (1, 2), (3, 4)]│
├─────┼────────────────────────┤
│ 3 │ [(1, 7), (2, 4)] │
└─────┴────────────────────────┘
Em Rust: Vec<Vec<(usize, i64)>>
BFS vs DFS
Grafo: BFS (largura): DFS (profundidade):
0 0 0
/ \ / \ /
1 2 1 2 1
/ \ \ / \ \ / \
3 4 5 3 4 5 3 4
\
Ordem: 0,1,2,3,4,5 0,1,3,4,5,2
Estrutura: Fila (FIFO) Pilha (LIFO)/Recursão
Complexidade
| Operação | Lista de Adjacência | Observação |
|---|---|---|
| Adicionar vértice | O(1) | Adiciona lista vazia |
| Adicionar aresta | O(1) | Push no vetor |
| Remover aresta | O(E/V) | Busca linear na lista |
| Verificar adjacência | O(E/V) | Busca linear na lista |
| BFS / DFS | O(V + E) | Visita cada vértice e aresta |
| Espaço | O(V + E) | Eficiente para grafos esparsos |
| Listar vizinhos | O(grau(v)) | Acesso direto à lista |
Nota: E/V representa o grau médio dos vértices.
Implementação em Rust
Grafo Simples com Vec
/// Grafo não-direcionado usando Vec<Vec<usize>>
struct GrafoSimples {
/// Lista de adjacência: adj[v] contém os vizinhos de v
adj: Vec<Vec<usize>>,
/// Número de vértices
num_vertices: usize,
/// Número de arestas
num_arestas: usize,
}
impl GrafoSimples {
/// Cria um grafo com n vértices e sem arestas
fn novo(num_vertices: usize) -> Self {
GrafoSimples {
adj: vec![Vec::new(); num_vertices],
num_vertices,
num_arestas: 0,
}
}
/// Adiciona uma aresta não-direcionada entre u e v
fn adicionar_aresta(&mut self, u: usize, v: usize) {
assert!(u < self.num_vertices && v < self.num_vertices);
self.adj[u].push(v);
self.adj[v].push(u);
self.num_arestas += 1;
}
/// Retorna os vizinhos do vértice v
fn vizinhos(&self, v: usize) -> &[usize] {
&self.adj[v]
}
/// Grau do vértice (número de conexões)
fn grau(&self, v: usize) -> usize {
self.adj[v].len()
}
/// Exibe o grafo
fn exibir(&self) {
println!("Grafo: {} vértices, {} arestas", self.num_vertices, self.num_arestas);
for (v, vizinhos) in self.adj.iter().enumerate() {
println!(" {} -> {:?}", v, vizinhos);
}
}
}
BFS (Busca em Largura)
use std::collections::VecDeque;
/// Realiza BFS a partir de um vértice de origem.
/// Retorna o vetor de distâncias (-1 se não alcançável).
fn bfs(grafo: &GrafoSimples, origem: usize) -> Vec<i32> {
let n = grafo.num_vertices;
let mut distancia = vec![-1i32; n];
let mut fila = VecDeque::new();
distancia[origem] = 0;
fila.push_back(origem);
while let Some(atual) = fila.pop_front() {
for &vizinho in grafo.vizinhos(atual) {
if distancia[vizinho] == -1 {
distancia[vizinho] = distancia[atual] + 1;
fila.push_back(vizinho);
}
}
}
distancia
}
/// BFS que também retorna o caminho (predecessores)
fn bfs_com_caminho(grafo: &GrafoSimples, origem: usize) -> (Vec<i32>, Vec<Option<usize>>) {
let n = grafo.num_vertices;
let mut distancia = vec![-1i32; n];
let mut predecessor: Vec<Option<usize>> = vec![None; n];
let mut fila = VecDeque::new();
distancia[origem] = 0;
fila.push_back(origem);
while let Some(atual) = fila.pop_front() {
for &vizinho in grafo.vizinhos(atual) {
if distancia[vizinho] == -1 {
distancia[vizinho] = distancia[atual] + 1;
predecessor[vizinho] = Some(atual);
fila.push_back(vizinho);
}
}
}
(distancia, predecessor)
}
/// Reconstrói o caminho de origem a destino usando predecessores
fn reconstruir_caminho(predecessor: &[Option<usize>], destino: usize) -> Vec<usize> {
let mut caminho = Vec::new();
let mut atual = Some(destino);
while let Some(v) = atual {
caminho.push(v);
atual = predecessor[v];
}
caminho.reverse();
caminho
}
DFS (Busca em Profundidade)
/// DFS iterativa usando pilha explícita
fn dfs_iterativa(grafo: &GrafoSimples, origem: usize) -> Vec<usize> {
let n = grafo.num_vertices;
let mut visitado = vec![false; n];
let mut ordem = Vec::new();
let mut pilha = vec![origem];
while let Some(atual) = pilha.pop() {
if visitado[atual] {
continue;
}
visitado[atual] = true;
ordem.push(atual);
// Adiciona vizinhos na pilha (em ordem reversa para manter ordem natural)
for &vizinho in grafo.vizinhos(atual).iter().rev() {
if !visitado[vizinho] {
pilha.push(vizinho);
}
}
}
ordem
}
/// DFS recursiva
fn dfs_recursiva(grafo: &GrafoSimples, origem: usize) -> Vec<usize> {
let n = grafo.num_vertices;
let mut visitado = vec![false; n];
let mut ordem = Vec::new();
fn dfs_helper(
grafo: &GrafoSimples,
v: usize,
visitado: &mut Vec<bool>,
ordem: &mut Vec<usize>,
) {
visitado[v] = true;
ordem.push(v);
for &vizinho in grafo.vizinhos(v) {
if !visitado[vizinho] {
dfs_helper(grafo, vizinho, visitado, ordem);
}
}
}
dfs_helper(grafo, origem, &mut visitado, &mut ordem);
ordem
}
Grafo Flexível com HashMap
use std::collections::HashMap;
/// Grafo genérico usando HashMap para rótulos flexíveis
struct Grafo<T: Eq + std::hash::Hash + Clone> {
/// Mapeia cada vértice para sua lista de vizinhos com pesos
adj: HashMap<T, Vec<(T, f64)>>,
/// Se o grafo é direcionado
direcionado: bool,
}
impl<T: Eq + std::hash::Hash + Clone + std::fmt::Debug> Grafo<T> {
fn novo(direcionado: bool) -> Self {
Grafo {
adj: HashMap::new(),
direcionado,
}
}
/// Adiciona um vértice ao grafo
fn adicionar_vertice(&mut self, v: T) {
self.adj.entry(v).or_insert_with(Vec::new);
}
/// Adiciona uma aresta com peso
fn adicionar_aresta(&mut self, de: T, para: T, peso: f64) {
self.adj.entry(de.clone()).or_default().push((para.clone(), peso));
if !self.direcionado {
self.adj.entry(para).or_default().push((de, peso));
} else {
// Garante que o vértice de destino exista
self.adj.entry(para).or_default();
}
}
/// Retorna os vizinhos de um vértice
fn vizinhos(&self, v: &T) -> Option<&Vec<(T, f64)>> {
self.adj.get(v)
}
/// Número de vértices
fn num_vertices(&self) -> usize {
self.adj.len()
}
/// Exibe o grafo
fn exibir(&self) {
let tipo = if self.direcionado { "Direcionado" } else { "Não-direcionado" };
println!("Grafo {} ({} vértices):", tipo, self.num_vertices());
for (v, vizinhos) in &self.adj {
let lista: Vec<String> = vizinhos
.iter()
.map(|(u, p)| format!("{:?}({})", u, p))
.collect();
println!(" {:?} -> [{}]", v, lista.join(", "));
}
}
}
Exemplo Prático: Rede Social e Busca de Rotas
use std::collections::{HashMap, HashSet, VecDeque};
/// Rede social simples usando grafo não-direcionado
struct RedeSocial {
/// Amizades: mapeia nome -> conjunto de amigos
amizades: HashMap<String, HashSet<String>>,
}
impl RedeSocial {
fn nova() -> Self {
RedeSocial {
amizades: HashMap::new(),
}
}
/// Adiciona um usuário à rede
fn adicionar_usuario(&mut self, nome: &str) {
self.amizades.entry(nome.to_string()).or_default();
}
/// Cria amizade bidirecional entre dois usuários
fn adicionar_amizade(&mut self, a: &str, b: &str) {
self.amizades.entry(a.to_string()).or_default().insert(b.to_string());
self.amizades.entry(b.to_string()).or_default().insert(a.to_string());
}
/// Retorna amigos diretos de um usuário
fn amigos(&self, nome: &str) -> Option<&HashSet<String>> {
self.amizades.get(nome)
}
/// Encontra amigos em comum entre dois usuários
fn amigos_em_comum(&self, a: &str, b: &str) -> HashSet<String> {
match (self.amizades.get(a), self.amizades.get(b)) {
(Some(amigos_a), Some(amigos_b)) => {
amigos_a.intersection(amigos_b).cloned().collect()
}
_ => HashSet::new(),
}
}
/// Sugere amigos: amigos de amigos que ainda não são amigos diretos
fn sugerir_amigos(&self, nome: &str) -> Vec<(String, usize)> {
let meus_amigos = match self.amizades.get(nome) {
Some(a) => a,
None => return vec![],
};
// Conta quantas conexões em comum cada potencial amigo tem
let mut sugestoes: HashMap<String, usize> = HashMap::new();
for amigo in meus_amigos {
if let Some(amigos_do_amigo) = self.amizades.get(amigo) {
for candidato in amigos_do_amigo {
// Não sugerir a si mesmo nem amigos existentes
if candidato != nome && !meus_amigos.contains(candidato) {
*sugestoes.entry(candidato.clone()).or_default() += 1;
}
}
}
}
// Ordena por número de conexões em comum (decrescente)
let mut resultado: Vec<(String, usize)> = sugestoes.into_iter().collect();
resultado.sort_by(|a, b| b.1.cmp(&a.1));
resultado
}
/// Grau de separação entre dois usuários (BFS)
fn grau_separacao(&self, de: &str, para: &str) -> Option<usize> {
if !self.amizades.contains_key(de) || !self.amizades.contains_key(para) {
return None;
}
if de == para {
return Some(0);
}
let mut visitado: HashSet<String> = HashSet::new();
let mut fila: VecDeque<(String, usize)> = VecDeque::new();
visitado.insert(de.to_string());
fila.push_back((de.to_string(), 0));
while let Some((atual, dist)) = fila.pop_front() {
if let Some(amigos) = self.amizades.get(&atual) {
for amigo in amigos {
if amigo == para {
return Some(dist + 1);
}
if visitado.insert(amigo.clone()) {
fila.push_back((amigo.clone(), dist + 1));
}
}
}
}
None // Não conectados
}
/// Exibe a rede social
fn exibir(&self) {
println!("╔══════════════════════════════╗");
println!("║ Rede Social ║");
println!("╠══════════════════════════════╣");
for (usuario, amigos) in &self.amizades {
let lista: Vec<&str> = amigos.iter().map(|s| s.as_str()).collect();
println!("║ {} ({} amigos): {:?}", usuario, amigos.len(), lista);
}
println!("╚══════════════════════════════╝");
}
}
fn main() {
// === Rede Social ===
let mut rede = RedeSocial::nova();
// Adicionando usuários e amizades
for nome in &["Alice", "Bruno", "Carol", "Daniel", "Eva", "Felipe", "Gabi"] {
rede.adicionar_usuario(nome);
}
rede.adicionar_amizade("Alice", "Bruno");
rede.adicionar_amizade("Alice", "Carol");
rede.adicionar_amizade("Bruno", "Carol");
rede.adicionar_amizade("Bruno", "Daniel");
rede.adicionar_amizade("Carol", "Eva");
rede.adicionar_amizade("Daniel", "Eva");
rede.adicionar_amizade("Daniel", "Felipe");
rede.adicionar_amizade("Eva", "Gabi");
rede.exibir();
// Amigos em comum
let comuns = rede.amigos_em_comum("Alice", "Daniel");
println!("\nAmigos em comum (Alice, Daniel): {:?}", comuns);
// Sugestões de amizade
let sugestoes = rede.sugerir_amigos("Alice");
println!("\nSugestões para Alice:");
for (nome, conexoes) in &sugestoes {
println!(" {} ({} amigos em comum)", nome, conexoes);
}
// Grau de separação
let pares = vec![("Alice", "Gabi"), ("Alice", "Bruno"), ("Alice", "Felipe")];
println!("\nGraus de separação:");
for (a, b) in pares {
match rede.grau_separacao(a, b) {
Some(grau) => println!(" {} <-> {}: {} grau(s)", a, b, grau),
None => println!(" {} <-> {}: não conectados", a, b),
}
}
}
Exercícios
Exercício 1: Detecção de Ciclos
Implemente uma função que detecta se um grafo não-direcionado contém ciclos. Use DFS e rastreie o vértice pai para distinguir arestas de retorno de arestas para o pai.
fn tem_ciclo(grafo: &GrafoSimples) -> bool {
// Dica: na DFS, se encontrar um vizinho já visitado que
// não seja o pai, há um ciclo
todo!()
}
Exercício 2: Componentes Conectados
Implemente uma função que encontra todos os componentes conectados de um grafo. Retorne um vetor onde cada elemento é um vetor de vértices pertencentes ao mesmo componente.
fn componentes_conectados(grafo: &GrafoSimples) -> Vec<Vec<usize>> {
// Dica: execute BFS/DFS de cada vértice não visitado
// Cada execução descobre um novo componente
todo!()
}
Exercício 3: Caminho Mais Curto com BFS
Implemente uma função que encontra o menor caminho entre dois vértices e exibe o caminho completo. Se não houver caminho, retorne uma mensagem apropriada.
fn menor_caminho(grafo: &GrafoSimples, origem: usize, destino: usize) -> Option<Vec<usize>> {
// Dica: use BFS com rastreamento de predecessores
// e reconstroi o caminho ao final
todo!()
}
Conclusão
A lista de adjacência é a representação de grafos mais utilizada na prática por diversos motivos:
- Eficiente em espaço O(V + E) – ideal para grafos esparsos
- Iteração rápida sobre vizinhos de um vértice
- Flexível: fácil adicionar vértices e arestas dinâmicamente
- Em Rust,
Vec<Vec<usize>>é simples e performático - Para rótulos flexíveis,
HashMap<T, Vec<T>>é a melhor escolha
A BFS e DFS são os algoritmos fundamentais sobre grafos, servindo como base para detecção de ciclos, componentes conectados, ordenação topológica e muito mais.
Na próxima página, veremos a Matriz de Adjacência, que é mais adequada para grafos densos.
Conteúdo produzido pela Equipe Rust Brasil para rustlang.com.br