Listas Ligadas em Rust: Desafios e Implementações

Guia completo sobre listas ligadas em Rust — por que são difíceis, implementação com Box, considerações sobre listas duplamente ligadas e quando usar std::collections::LinkedList.

Introdução

Listas ligadas são uma das estruturas de dados mais clássicas da ciência da computação. Em linguagens como C, Java ou Python, implementá-las é quase trivial. Em Rust, porém, essa tarefa se torna um exercício profundo sobre o sistema de ownership, borrowing e lifetimes.

A dificuldade não é um defeito — é uma revelação. As listas ligadas violam naturalmente muitas garantias que Rust busca oferecer: cada nó precisa apontar para o próximo, criando relações de propriedade complexas. Listas duplamente ligadas introduzem referências cíclicas, que são fundamentalmente incompatíveis com o modelo de ownership simples de Rust.

Este artigo é inspirado pelo famoso tutorial “Learn Rust With Entirely Too Many Linked Lists” de Alexis Beresford, uma referência essencial para quem quer entender Rust profundamente. Aqui, abordaremos os conceitos teóricos, a implementação prática e, crucialmente, discutiremos quando não usar listas ligadas em Rust.


Conceito e Teoria

Lista Simplesmente Ligada

Cada nó contém um valor e um ponteiro para o próximo nó. O último nó aponta para “nada” (None):

  head
   │
   ▼
  ┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
  │ valor: 10  │    │ valor: 20  │    │ valor: 30  │    │ valor: 40  │
  │ next: ─────┼───►│ next: ─────┼───►│ next: ─────┼───►│ next: None │
  └────────────┘    └────────────┘    └────────────┘    └────────────┘
       Nó 0              Nó 1              Nó 2              Nó 3

  Cada nó está em uma posição DIFERENTE na heap.
  Não há garantia de que estejam próximos na memória.

Lista Duplamente Ligada

Cada nó possui ponteiros tanto para o próximo quanto para o anterior:

  head                                                           tail
   │                                                              │
   ▼                                                              ▼
  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐    ┌──────────────┐
  │ prev: None   │◄────┤ prev: ───────│◄────┤ prev: ───────│◄───┤ prev: ───────│
  │ valor: 10    │     │ valor: 20    │     │ valor: 30    │    │ valor: 40    │
  │ next: ───────┼────►│ next: ───────┼────►│ next: ───────┼───►│ next: None   │
  └──────────────┘     └──────────────┘     └──────────────┘    └──────────────┘

Layout de Memória: Vec vs Lista Ligada

  Vec<i32> — memória contígua (excelente para cache):
  ┌────┬────┬────┬────┬────┐
  │ 10 │ 20 │ 30 │ 40 │ 50 │  ← tudo junto na heap
  └────┴────┴────┴────┴────┘
  Endereços: 0x1000, 0x1004, 0x1008, 0x100C, 0x1010

  LinkedList<i32> — nós espalhados na heap:
  0x2000: [10, ptr] ──► 0x3050: [20, ptr] ──► 0x2F00: [30, ptr] ──► ...
  │                     │                     │
  Cada acesso ao        Os endereços podem    Cache misses frequentes!
  próximo nó pode       estar muito distantes
  causar cache miss

Por Que Listas Ligadas São Difíceis em Rust?

O sistema de ownership do Rust impõe regras que entram em conflito com a natureza das listas:

  1. Cada valor tem exatamente um dono: Em uma lista simplesmente ligada, cada nó “possui” o próximo. Isso funciona com Box<T>. Mas quem possui a lista inteira?

  2. Referências cíclicas são proibidas: Uma lista duplamente ligada cria ciclos: A aponta para B, que aponta de volta para A. Isso é impossível com referências simples.

  3. Mutabilidade compartilhada: Para inserir no meio da lista, você precisa de acesso mutável a um nó enquanto outro nó ainda referencia a lista.


Complexidade

OperaçãoLista SimplesLista DuplaVec
Acesso por índiceO(n)O(n)O(1)
BuscaO(n)O(n)O(n)
Inserção no inícioO(1)O(1)O(n)
Inserção no final (com tail)O(1)O(1)O(1) amortizado
Inserção no meio (com ponteiro)O(1)O(1)O(n)
Remoção no inícioO(1)O(1)O(n)
Remoção no finalO(n)O(1)O(1)
Remoção no meio (com ponteiro)O(1)*O(1)O(n)
Uso de memória por elementoAltoMuito altoBaixo

*Na lista simples, remover um nó requer acesso ao nó anterior, o que pode custar O(n) se não tivermos esse ponteiro.


Implementação em Rust

Lista Simplesmente Ligada com Box

Esta é a implementação mais idiomática e segura em Rust:

use std::fmt;

/// Tipo auxiliar para o ponteiro para o próximo nó
type Link<T> = Option<Box<No<T>>>;

/// Nó da lista ligada
#[derive(Debug)]
struct No<T> {
    valor: T,
    proximo: Link<T>,
}

/// Lista simplesmente ligada
#[derive(Debug)]
pub struct ListaLigada<T> {
    cabeca: Link<T>,
    tamanho: usize,
}

impl<T> ListaLigada<T> {
    /// Cria uma lista vazia
    pub fn nova() -> Self {
        ListaLigada {
            cabeca: None,
            tamanho: 0,
        }
    }

    /// Retorna o número de elementos na lista
    pub fn tamanho(&self) -> usize {
        self.tamanho
    }

    /// Verifica se a lista está vazia
    pub fn esta_vazia(&self) -> bool {
        self.tamanho == 0
    }

    /// Insere um elemento no início da lista — O(1)
    pub fn inserir_inicio(&mut self, valor: T) {
        let novo_no = Box::new(No {
            valor,
            // O novo nó assume a propriedade (ownership) da cabeça anterior
            proximo: self.cabeca.take(),
        });
        self.cabeca = Some(novo_no);
        self.tamanho += 1;
    }

    /// Remove e retorna o elemento do início da lista — O(1)
    pub fn remover_inicio(&mut self) -> Option<T> {
        // take() substitui self.cabeca por None e retorna o valor anterior
        self.cabeca.take().map(|no| {
            self.cabeca = no.proximo;
            self.tamanho -= 1;
            no.valor
        })
    }

    /// Retorna uma referência ao primeiro elemento — O(1)
    pub fn espiar(&self) -> Option<&T> {
        self.cabeca.as_ref().map(|no| &no.valor)
    }

    /// Retorna uma referência mutável ao primeiro elemento — O(1)
    pub fn espiar_mut(&mut self) -> Option<&mut T> {
        self.cabeca.as_mut().map(|no| &mut no.valor)
    }

    /// Insere um elemento no final da lista — O(n)
    pub fn inserir_final(&mut self, valor: T) {
        let novo_no = Box::new(No {
            valor,
            proximo: None,
        });

        // Caso especial: lista vazia
        if self.cabeca.is_none() {
            self.cabeca = Some(novo_no);
            self.tamanho += 1;
            return;
        }

        // Percorre até o último nó
        let mut atual = &mut self.cabeca;
        while let Some(ref mut no) = atual {
            if no.proximo.is_none() {
                no.proximo = Some(novo_no);
                self.tamanho += 1;
                return;
            }
            atual = &mut no.proximo;
        }
    }

    /// Verifica se a lista contém um determinado valor — O(n)
    pub fn contem(&self, alvo: &T) -> bool
    where
        T: PartialEq,
    {
        let mut atual = &self.cabeca;
        while let Some(ref no) = atual {
            if &no.valor == alvo {
                return true;
            }
            atual = &no.proximo;
        }
        false
    }

    /// Converte a lista em um Vec — O(n)
    pub fn para_vec(&self) -> Vec<&T> {
        let mut resultado = Vec::with_capacity(self.tamanho);
        let mut atual = &self.cabeca;
        while let Some(ref no) = atual {
            resultado.push(&no.valor);
            atual = &no.proximo;
        }
        resultado
    }
}

/// Implementação do iterador para consumir a lista
impl<T> Iterator for ListaLigada<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        self.remover_inicio()
    }
}

/// Implementação do Display para visualização
impl<T: fmt::Display> fmt::Display for ListaLigada<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut atual = &self.cabeca;
        write!(f, "[")?;
        let mut primeiro = true;
        while let Some(ref no) = atual {
            if !primeiro {
                write!(f, " → ")?;
            }
            write!(f, "{}", no.valor)?;
            primeiro = false;
            atual = &no.proximo;
        }
        write!(f, "]")
    }
}

/// Libera a memória iterativamente para evitar estouro de pilha
/// em listas muito grandes (o Drop padrão seria recursivo)
impl<T> Drop for ListaLigada<T> {
    fn drop(&mut self) {
        let mut link_atual = self.cabeca.take();
        while let Some(mut no) = link_atual {
            link_atual = no.proximo.take();
            // 'no' é descartado aqui sem recursão
        }
    }
}

fn main() {
    let mut lista = ListaLigada::nova();

    // Inserções no início
    lista.inserir_inicio(30);
    lista.inserir_inicio(20);
    lista.inserir_inicio(10);
    println!("Após inserções no início: {}", lista);

    // Inserção no final
    lista.inserir_final(40);
    lista.inserir_final(50);
    println!("Após inserções no final: {}", lista);

    // Espiar o primeiro elemento
    if let Some(primeiro) = lista.espiar() {
        println!("Primeiro elemento: {}", primeiro);
    }

    // Remover do início
    let removido = lista.remover_inicio();
    println!("Removido: {:?}", removido);
    println!("Lista após remoção: {}", lista);

    // Verificar se contém
    println!("Contém 30? {}", lista.contem(&30));
    println!("Contém 99? {}", lista.contem(&99));

    // Tamanho
    println!("Tamanho: {}", lista.tamanho());

    // Converter para Vec
    println!("Como Vec: {:?}", lista.para_vec());
}

Iterador por Referência (Sem Consumir a Lista)

/// Iterador que empresta elementos da lista (não consome)
pub struct Iter<'a, T> {
    proximo: Option<&'a No<T>>,
}

impl<T> ListaLigada<T> {
    /// Retorna um iterador por referência imutável
    pub fn iter(&self) -> Iter<'_, T> {
        Iter {
            proximo: self.cabeca.as_deref(),
        }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.proximo.map(|no| {
            self.proximo = no.proximo.as_deref();
            &no.valor
        })
    }
}

// Uso:
// for valor in lista.iter() {
//     println!("{}", valor);
// }

Métodos Principais

Nossa Implementação

MétodoComplexidadeDescrição
nova()O(1)Cria uma lista vazia
inserir_inicio(v)O(1)Adiciona no início
remover_inicio()O(1)Remove e retorna o primeiro elemento
inserir_final(v)O(n)Adiciona no final (sem ponteiro tail)
espiar()O(1)Referência ao primeiro elemento
contem(v)O(n)Busca linear
para_vec()O(n)Converte para vetor de referências
tamanho()O(1)Retorna o número de elementos
esta_vazia()O(1)Verifica se está vazia
iter()O(1)Cria iterador por referência

std::collections::LinkedList

A biblioteca padrão oferece uma lista duplamente ligada:

MétodoComplexidadeDescrição
push_front(v)O(1)Insere no início
push_back(v)O(1)Insere no final
pop_front()O(1)Remove do início
pop_back()O(1)Remove do final
front() / back()O(1)Referência ao primeiro/último
len()O(1)Número de elementos
contains(v)O(n)Busca linear
iter()O(1)Cria iterador

Exemplo Prático: Sistema de Histórico de Desfazer (Undo)

Um caso de uso clássico de listas ligadas é implementar um sistema de undo/redo. Cada ação é empilhada na lista, e desfazer remove a ação mais recente.

use std::fmt;

type Link<T> = Option<Box<No<T>>>;

struct No<T> {
    valor: T,
    proximo: Link<T>,
}

struct Pilha<T> {
    topo: Link<T>,
    tamanho: usize,
}

impl<T> Pilha<T> {
    fn nova() -> Self {
        Pilha { topo: None, tamanho: 0 }
    }

    fn empilhar(&mut self, valor: T) {
        let novo = Box::new(No {
            valor,
            proximo: self.topo.take(),
        });
        self.topo = Some(novo);
        self.tamanho += 1;
    }

    fn desempilhar(&mut self) -> Option<T> {
        self.topo.take().map(|no| {
            self.topo = no.proximo;
            self.tamanho -= 1;
            no.valor
        })
    }

    fn espiar(&self) -> Option<&T> {
        self.topo.as_ref().map(|no| &no.valor)
    }

    fn esta_vazia(&self) -> bool {
        self.tamanho == 0
    }
}

/// Representa uma ação que pode ser desfeita
#[derive(Debug, Clone)]
enum Acao {
    InserirTexto { posicao: usize, texto: String },
    RemoverTexto { posicao: usize, texto: String },
    FormatarNegrito { inicio: usize, fim: usize },
    FormatarItalico { inicio: usize, fim: usize },
    SubstituirTexto { posicao: usize, antigo: String, novo: String },
}

impl fmt::Display for Acao {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Acao::InserirTexto { posicao, texto } => {
                write!(f, "Inserir '{}' na posição {}", texto, posicao)
            }
            Acao::RemoverTexto { posicao, texto } => {
                write!(f, "Remover '{}' da posição {}", texto, posicao)
            }
            Acao::FormatarNegrito { inicio, fim } => {
                write!(f, "Negrito em [{}, {})", inicio, fim)
            }
            Acao::FormatarItalico { inicio, fim } => {
                write!(f, "Itálico em [{}, {})", inicio, fim)
            }
            Acao::SubstituirTexto { posicao, antigo, novo } => {
                write!(f, "Substituir '{}' por '{}' na posição {}", antigo, novo, posicao)
            }
        }
    }
}

/// Inverte uma ação para gerar a ação de desfazer
fn inverter_acao(acao: &Acao) -> Acao {
    match acao {
        Acao::InserirTexto { posicao, texto } => Acao::RemoverTexto {
            posicao: *posicao,
            texto: texto.clone(),
        },
        Acao::RemoverTexto { posicao, texto } => Acao::InserirTexto {
            posicao: *posicao,
            texto: texto.clone(),
        },
        Acao::FormatarNegrito { inicio, fim } => Acao::FormatarNegrito {
            inicio: *inicio,
            fim: *fim,
        },
        Acao::FormatarItalico { inicio, fim } => Acao::FormatarItalico {
            inicio: *inicio,
            fim: *fim,
        },
        Acao::SubstituirTexto { posicao, antigo, novo } => Acao::SubstituirTexto {
            posicao: *posicao,
            antigo: novo.clone(),
            novo: antigo.clone(),
        },
    }
}

/// Sistema de histórico com undo e redo
struct HistoricoEditor {
    historico_desfazer: Pilha<Acao>,
    historico_refazer: Pilha<Acao>,
}

impl HistoricoEditor {
    fn novo() -> Self {
        HistoricoEditor {
            historico_desfazer: Pilha::nova(),
            historico_refazer: Pilha::nova(),
        }
    }

    /// Registra uma nova ação (limpa o histórico de refazer)
    fn executar(&mut self, acao: Acao) {
        println!("  ✔ Executando: {}", acao);
        self.historico_desfazer.empilhar(acao);
        // Ao executar nova ação, o histórico de refazer é invalidado
        self.historico_refazer = Pilha::nova();
    }

    /// Desfaz a última ação
    fn desfazer(&mut self) -> Option<Acao> {
        self.historico_desfazer.desempilhar().map(|acao| {
            let acao_inversa = inverter_acao(&acao);
            println!("  ↩ Desfazendo: {} → Aplicando: {}", acao, acao_inversa);
            self.historico_refazer.empilhar(acao.clone());
            acao_inversa
        })
    }

    /// Refaz a última ação desfeita
    fn refazer(&mut self) -> Option<Acao> {
        self.historico_refazer.desempilhar().map(|acao| {
            println!("  ↪ Refazendo: {}", acao);
            self.historico_desfazer.empilhar(acao.clone());
            acao
        })
    }

    /// Mostra o estado atual
    fn estado(&self) {
        print!("  Estado: ");
        if let Some(ultima) = self.historico_desfazer.espiar() {
            println!("última ação = {}", ultima);
        } else {
            println!("nenhuma ação no histórico");
        }
    }
}

fn main() {
    let mut editor = HistoricoEditor::novo();

    println!("═══ Simulação de Editor de Texto ═══\n");

    // Usuário digita e formata texto
    println!("── Executando ações ──");
    editor.executar(Acao::InserirTexto {
        posicao: 0,
        texto: "Olá, ".to_string(),
    });
    editor.executar(Acao::InserirTexto {
        posicao: 5,
        texto: "mundo!".to_string(),
    });
    editor.executar(Acao::FormatarNegrito { inicio: 0, fim: 3 });
    editor.executar(Acao::SubstituirTexto {
        posicao: 5,
        antigo: "mundo".to_string(),
        novo: "Rust".to_string(),
    });

    editor.estado();

    // Desfazer duas vezes
    println!("\n── Desfazendo ──");
    editor.desfazer();
    editor.desfazer();
    editor.estado();

    // Refazer uma vez
    println!("\n── Refazendo ──");
    editor.refazer();
    editor.estado();

    // Nova ação após desfazer (invalida o refazer)
    println!("\n── Nova ação (invalida refazer) ──");
    editor.executar(Acao::FormatarItalico { inicio: 0, fim: 11 });

    // Tentar refazer (não deve funcionar)
    println!("\n── Tentando refazer ──");
    match editor.refazer() {
        Some(acao) => println!("  Refez: {}", acao),
        None => println!("  Nada para refazer (histórico foi invalidado)"),
    }
}

Comparação com Outras Estruturas

CritérioLista Ligada (Box)VecVecDeque
Inserção inícioO(1)O(n)O(1) amortizado
Inserção finalO(n) ou O(1)*O(1) amortizadoO(1) amortizado
Acesso aleatórioO(n)O(1)O(1)
Cache-friendlyNaoSimSim
Overhead por elemento~8-16 bytes (ptr)00
Fragmentação de memóriaAltaNenhumaNenhuma
Propriedade (ownership)Clara (linear)ClaraClara

*O(1) no final se mantivermos um ponteiro tail, mas isso complica o ownership em Rust.

Quando Usar Lista Ligada em Rust?

Raramente. Na prática, Vec<T> ou VecDeque<T> são quase sempre superiores devido à localidade de cache. Use listas ligadas quando:

  1. Precisa de inserção/remoção O(1) no início e não pode usar VecDeque por algum motivo.
  2. Está implementando estruturas mais complexas como grafos ou árvores internamente.
  3. Fins educacionais — aprender sobre ownership e lifetimes em Rust.
  4. Precisa dividir e juntar listas frequentementesplit_off e append são O(1) em listas ligadas mas O(n) em vetores.
  5. Elementos são muito grandes e a realocação/cópia de Vec seria custosa.

O Conselho da Comunidade Rust

A comunidade Rust geralmente recomenda: “Se você está pensando em usar uma lista ligada, use um Vec.” O próprio std::collections::LinkedList inclui um aviso na documentação de que é quase sempre inferior ao Vec ou VecDeque.


Exercícios

Exercício 1: Reverter uma Lista Ligada

Implemente uma função reverter(lista: &mut ListaLigada<T>) que inverte a ordem dos elementos da lista in-place, sem alocar uma nova lista. A complexidade deve ser O(n) de tempo e O(1) de espaço adicional.

Dica: Use três ponteiros (anterior, atual, próximo) e vá redirecionando os links.

Exercício 2: Detectar Ciclo (Conceitual)

Embora nossa implementação com Box não permita ciclos (o sistema de ownership impede), descreva o algoritmo de Floyd (tartaruga e lebre) para detecção de ciclos. Implemente uma versão que funcione com índices em um Vec<Option<usize>> simulando os ponteiros.

Dica: Use dois “ponteiros” (índices): um avança um passo por vez, outro avança dois. Se se encontrarem, há um ciclo.

Exercício 3: Merge de Duas Listas Ordenadas

Implemente uma função merge_ordenado(l1: ListaLigada<i32>, l2: ListaLigada<i32>) -> ListaLigada<i32> que recebe duas listas ordenadas e retorna uma nova lista ordenada contendo todos os elementos de ambas. A complexidade deve ser O(n + m).

Dica: Consuma ambas as listas usando remover_inicio() e compare os valores para decidir qual inserir primeiro na lista resultado.


Conclusão

Listas ligadas em Rust são um excelente exercício de aprendizado, mas raramente a melhor escolha para código de produção. As principais lições deste artigo:

  1. O sistema de ownership do Rust expõe problemas reais que outras linguagens escondem (referências cíclicas, mutabilidade compartilhada).

  2. Listas simplesmente ligadas com Box<T> são seguras e relativamente simples de implementar, mas limitadas (sem acesso eficiente ao final).

  3. Listas duplamente ligadas requerem Rc<RefCell<T>> ou código unsafe, aumentando consideravelmente a complexidade.

  4. Na prática, prefira Vec<T> ou VecDeque<T> — a localidade de cache supera as vantagens teóricas de listas ligadas na maioria dos cenários.

  5. Para aprofundamento, leia “Learn Rust With Entirely Too Many Linked Lists”, que explora seis variações de listas ligadas e é uma das melhores formas de dominar conceitos avançados de Rust.

A verdadeira sabedoria em estruturas de dados não é saber implementar todas elas, mas saber quando cada uma é a escolha certa para o problema em questão.