Árvore Binária

Entenda os fundamentos das árvores binárias em Rust: terminologia, implementação com enum e Box, e os quatro tipos de travessia (in-order, pre-order, post-order, level-order).

Introdução

A árvore binária é uma das estruturas de dados mais fundamentais da ciência da computação. Ela organiza elementos de forma hierárquica, onde cada nó pode ter no máximo dois filhos: um à esquerda e outro à direita. Essa estrutura serve de base para diversas outras árvores especializadas, como BSTs, árvores AVL e heaps.

Em Rust, a implementação de árvores binárias é um excelente exercício para compreender o sistema de ownership, o uso de Box<T> para alocação no heap e o pattern matching com enum. Neste artigo, exploraremos a teoria por trás das árvores binárias, implementaremos uma versão completa em Rust e veremos aplicações práticas.


Conceito e Teoria

Terminologia Fundamental

Antes de mergulhar na implementação, é essencial entender a terminologia das árvores:

            Raiz (Root)
              [10]              <- Nível 0 (Profundidade 0)
             /    \
           [5]    [15]          <- Nível 1 (Profundidade 1)
          /   \      \
        [3]   [7]   [20]       <- Nível 2 (Profundidade 2)
        /       \
      [1]      [8]             <- Nível 3 (Profundidade 3)
       ^        ^
     Folhas (sem filhos)
  • Raiz (Root): O nó no topo da árvore, sem pai. No exemplo acima, é o nó [10].
  • Folha (Leaf): Um nó sem filhos. No exemplo, [1], [8] e [20] são folhas.
  • Altura (Height): O comprimento do caminho mais longo da raiz até uma folha. A árvore acima tem altura 3.
  • Profundidade (Depth): A distância de um nó até a raiz. O nó [7] tem profundidade 2.
  • Grau: O número de filhos de um nó. Em uma árvore binária, o grau máximo é 2.
  • Subárvore: Qualquer nó junto com todos os seus descendentes forma uma subárvore.

Tipos de Árvores Binárias

  Árvore Completa         Árvore Cheia         Árvore Degenerada
      [A]                    [A]                    [A]
     /   \                  /   \                     \
   [B]   [C]              [B]   [C]                  [B]
  /   \                  / \   / \                      \
[D]   [E]              [D][E][F][G]                    [C]
                                                          \
Todos os níveis       Cada nó tem            Cada nó tem    [D]
preenchidos da        0 ou 2 filhos          apenas 1 filho
esquerda p/ direita
  • Completa: Todos os níveis preenchidos, exceto possivelmente o último, que é preenchido da esquerda para a direita.
  • Cheia (Full): Todo nó tem exatamente 0 ou 2 filhos.
  • Degenerada: Cada nó tem no máximo 1 filho, comportando-se como uma lista encadeada.

Complexidade

OperaçãoMelhor CasoCaso MédioPior Caso
BuscaO(1)O(n)O(n)
InserçãoO(1)O(n)O(n)
RemoçãoO(1)O(n)O(n)
Travessia (qualquer)O(n)O(n)O(n)
AlturaO(n)O(n)O(n)
Contar nósO(n)O(n)O(n)
EspaçoO(n)O(n)

Nota: Em uma árvore binária genérica (sem ordenação), a busca exige percorrer todos os nós. Árvores de busca binária (BSTs) melhoram isso para O(log n) no caso médio.


Implementação em Rust

A seguir, implementamos uma árvore binária genérica com suporte a todas as operações fundamentais e os quatro tipos de travessia.

use std::collections::VecDeque;
use std::fmt::Display;

/// Representa uma árvore binária usando enum recursivo.
/// `Vazia` indica ausência de nó, e `No` contém o valor e dois filhos.
#[derive(Debug, Clone)]
enum ArvoreBinaria<T> {
    Vazia,
    No {
        valor: T,
        esquerda: Box<ArvoreBinaria<T>>,
        direita: Box<ArvoreBinaria<T>>,
    },
}

impl<T: Display + Clone> ArvoreBinaria<T> {
    /// Cria uma árvore vazia
    fn nova() -> Self {
        ArvoreBinaria::Vazia
    }

    /// Cria um nó folha (sem filhos)
    fn folha(valor: T) -> Self {
        ArvoreBinaria::No {
            valor,
            esquerda: Box::new(ArvoreBinaria::Vazia),
            direita: Box::new(ArvoreBinaria::Vazia),
        }
    }

    /// Cria um nó com filhos especificados
    fn novo_no(valor: T, esquerda: ArvoreBinaria<T>, direita: ArvoreBinaria<T>) -> Self {
        ArvoreBinaria::No {
            valor,
            esquerda: Box::new(esquerda),
            direita: Box::new(direita),
        }
    }

    /// Verifica se a árvore está vazia
    fn esta_vazia(&self) -> bool {
        matches!(self, ArvoreBinaria::Vazia)
    }

    /// Retorna a altura da árvore (-1 para árvore vazia)
    fn altura(&self) -> i32 {
        match self {
            ArvoreBinaria::Vazia => -1,
            ArvoreBinaria::No { esquerda, direita, .. } => {
                let alt_esq = esquerda.altura();
                let alt_dir = direita.altura();
                1 + alt_esq.max(alt_dir)
            }
        }
    }

    /// Conta o número total de nós na árvore
    fn contar_nos(&self) -> usize {
        match self {
            ArvoreBinaria::Vazia => 0,
            ArvoreBinaria::No { esquerda, direita, .. } => {
                1 + esquerda.contar_nos() + direita.contar_nos()
            }
        }
    }

    /// Conta o número de folhas (nós sem filhos)
    fn contar_folhas(&self) -> usize {
        match self {
            ArvoreBinaria::Vazia => 0,
            ArvoreBinaria::No { esquerda, direita, .. } => {
                if esquerda.esta_vazia() && direita.esta_vazia() {
                    1
                } else {
                    esquerda.contar_folhas() + direita.contar_folhas()
                }
            }
        }
    }

    // === TRAVESSIAS ===

    /// Travessia em pré-ordem: raiz -> esquerda -> direita
    fn pre_ordem(&self, resultado: &mut Vec<T>) {
        if let ArvoreBinaria::No { valor, esquerda, direita } = self {
            resultado.push(valor.clone());
            esquerda.pre_ordem(resultado);
            direita.pre_ordem(resultado);
        }
    }

    /// Travessia em ordem (in-order): esquerda -> raiz -> direita
    fn em_ordem(&self, resultado: &mut Vec<T>) {
        if let ArvoreBinaria::No { valor, esquerda, direita } = self {
            esquerda.em_ordem(resultado);
            resultado.push(valor.clone());
            direita.em_ordem(resultado);
        }
    }

    /// Travessia em pós-ordem: esquerda -> direita -> raiz
    fn pos_ordem(&self, resultado: &mut Vec<T>) {
        if let ArvoreBinaria::No { valor, esquerda, direita } = self {
            esquerda.pos_ordem(resultado);
            direita.pos_ordem(resultado);
            resultado.push(valor.clone());
        }
    }

    /// Travessia em nível (level-order / BFS)
    fn em_nivel(&self) -> Vec<T> {
        let mut resultado = Vec::new();
        let mut fila: VecDeque<&ArvoreBinaria<T>> = VecDeque::new();
        fila.push_back(self);

        while let Some(atual) = fila.pop_front() {
            if let ArvoreBinaria::No { valor, esquerda, direita } = atual {
                resultado.push(valor.clone());
                if !esquerda.esta_vazia() {
                    fila.push_back(esquerda);
                }
                if !direita.esta_vazia() {
                    fila.push_back(direita);
                }
            }
        }

        resultado
    }

    /// Exibe a árvore de forma visual (rotacionada 90 graus)
    fn exibir(&self, prefixo: &str, eh_direita: bool) {
        if let ArvoreBinaria::No { valor, esquerda, direita } = self {
            let conector = if eh_direita { "├── " } else { "└── " };
            let extensao = if eh_direita { "│   " } else { "    " };

            println!("{}{}{}", prefixo, conector, valor);

            let novo_prefixo = format!("{}{}", prefixo, extensao);
            direita.exibir(&novo_prefixo, true);
            esquerda.exibir(&novo_prefixo, false);
        }
    }
}

fn main() {
    // Construção manual da árvore:
    //         1
    //        / \
    //       2   3
    //      / \   \
    //     4   5   6
    let arvore = ArvoreBinaria::novo_no(
        1,
        ArvoreBinaria::novo_no(
            2,
            ArvoreBinaria::folha(4),
            ArvoreBinaria::folha(5),
        ),
        ArvoreBinaria::novo_no(
            3,
            ArvoreBinaria::Vazia,
            ArvoreBinaria::folha(6),
        ),
    );

    println!("=== Árvore Binária ===");
    println!("Altura: {}", arvore.altura());
    println!("Total de nós: {}", arvore.contar_nos());
    println!("Total de folhas: {}", arvore.contar_folhas());

    // Travessias
    let mut pre = Vec::new();
    arvore.pre_ordem(&mut pre);
    println!("\nPré-ordem:  {:?}", pre);

    let mut em = Vec::new();
    arvore.em_ordem(&mut em);
    println!("Em ordem:   {:?}", em);

    let mut pos = Vec::new();
    arvore.pos_ordem(&mut pos);
    println!("Pós-ordem:  {:?}", pos);

    let nivel = arvore.em_nivel();
    println!("Em nível:   {:?}", nivel);

    // Visualização
    println!("\nVisualização da árvore:");
    arvore.exibir("", false);
}

Métodos Principais

MétodoDescrição
nova()Cria uma árvore vazia
folha(valor)Cria um nó folha contendo o valor informado
novo_no(v, e, d)Cria um nó com valor v, subárvore esquerda e e direita d
esta_vazia()Retorna true se a árvore é vazia
altura()Calcula a altura da árvore recursivamente
contar_nos()Retorna o número total de nós
contar_folhas()Retorna o número de nós folha
pre_ordem()Travessia: raiz, esquerda, direita
em_ordem()Travessia: esquerda, raiz, direita
pos_ordem()Travessia: esquerda, direita, raiz
em_nivel()Travessia por largura (BFS) usando fila

Entendendo as Travessias

Árvore:        [A]
              /   \
            [B]   [C]
           /   \
         [D]   [E]

Pré-ordem  (Raiz-Esq-Dir):  A, B, D, E, C
Em ordem   (Esq-Raiz-Dir):  D, B, E, A, C
Pós-ordem  (Esq-Dir-Raiz):  D, E, B, C, A
Em nível   (BFS):           A, B, C, D, E

Exemplo Prático: Árvore de Expressões Aritméticas

Uma aplicação clássica de árvores binárias é representar e avaliar expressões matemáticas. Cada nó interno é um operador e cada folha é um operando.

/// Representa um token de expressão: número ou operador
#[derive(Debug, Clone)]
enum Token {
    Numero(f64),
    Operador(char), // '+', '-', '*', '/'
}

/// Árvore de expressão aritmética
#[derive(Debug, Clone)]
enum ArvoreExpressao {
    Vazia,
    No {
        token: Token,
        esquerda: Box<ArvoreExpressao>,
        direita: Box<ArvoreExpressao>,
    },
}

impl ArvoreExpressao {
    /// Cria um nó folha numérico
    fn numero(valor: f64) -> Self {
        ArvoreExpressao::No {
            token: Token::Numero(valor),
            esquerda: Box::new(ArvoreExpressao::Vazia),
            direita: Box::new(ArvoreExpressao::Vazia),
        }
    }

    /// Cria um nó operador com duas subárvores
    fn operador(op: char, esq: ArvoreExpressao, dir: ArvoreExpressao) -> Self {
        ArvoreExpressao::No {
            token: Token::Operador(op),
            esquerda: Box::new(esq),
            direita: Box::new(dir),
        }
    }

    /// Avalia a expressão representada pela árvore
    fn avaliar(&self) -> f64 {
        match self {
            ArvoreExpressao::Vazia => 0.0,
            ArvoreExpressao::No { token, esquerda, direita } => {
                match token {
                    Token::Numero(n) => *n,
                    Token::Operador(op) => {
                        let esq = esquerda.avaliar();
                        let dir = direita.avaliar();
                        match op {
                            '+' => esq + dir,
                            '-' => esq - dir,
                            '*' => esq * dir,
                            '/' => {
                                if dir == 0.0 {
                                    panic!("Divisão por zero!");
                                }
                                esq / dir
                            }
                            _ => panic!("Operador desconhecido: {}", op),
                        }
                    }
                }
            }
        }
    }

    /// Gera a expressão em notação infixa com parênteses
    fn para_infixa(&self) -> String {
        match self {
            ArvoreExpressao::Vazia => String::new(),
            ArvoreExpressao::No { token, esquerda, direita } => {
                match token {
                    Token::Numero(n) => format!("{}", n),
                    Token::Operador(op) => {
                        format!(
                            "({} {} {})",
                            esquerda.para_infixa(),
                            op,
                            direita.para_infixa()
                        )
                    }
                }
            }
        }
    }
}

fn main() {
    // Expressão: (3 + 4) * (10 - 2)
    //
    //         [*]
    //        /   \
    //      [+]   [-]
    //     / \   /   \
    //   [3] [4] [10] [2]

    let expressao = ArvoreExpressao::operador(
        '*',
        ArvoreExpressao::operador(
            '+',
            ArvoreExpressao::numero(3.0),
            ArvoreExpressao::numero(4.0),
        ),
        ArvoreExpressao::operador(
            '-',
            ArvoreExpressao::numero(10.0),
            ArvoreExpressao::numero(2.0),
        ),
    );

    println!("Expressão: {}", expressao.para_infixa());
    println!("Resultado: {}", expressao.avaliar());
    // Saída: Expressão: ((3 + 4) * (10 - 2))
    // Saída: Resultado: 56
}

Comparação com Outras Estruturas

CritérioÁrvore BináriaLista EncadeadaArray/Vec
Busca (sem ordem)O(n)O(n)O(n)
Inserção (posição)O(1)*O(1)*O(n)
Representação hierárquicaExcelenteRuimRuim
Uso de memóriaMédioMédioBaixo
Travessia ordenadaSim (in-order)Sim (linear)Sim (index)

* Quando a posição já é conhecida.

Quando usar Árvore Binária?

  • Use quando os dados são naturalmente hierárquicos (expressões, árvores de decisão, DOM).
  • Use como base para estruturas mais especializadas (BST, AVL, heap).
  • Não use para busca eficiente — prefira uma BST ou tabela hash.
  • Não use quando a ordem de inserção é mais importante que a hierarquia — prefira listas ou filas.

Exercícios

Exercício 1: Espelhamento de Árvore

Implemente um método espelhar() que inverta a árvore binária (troque todos os filhos esquerdos pelos direitos recursivamente). Após espelhar, a travessia em ordem deve retornar os valores na ordem inversa da original.

Original:       Espelhada:
    [1]            [1]
   /   \          /   \
 [2]   [3]      [3]   [2]
 / \              / \
[4] [5]         [5] [4]

Exercício 2: Verificar se Duas Árvores são Iguais

Escreva uma função sao_iguais(a, b) -> bool que compare duas árvores binárias e retorne true se possuem a mesma estrutura e os mesmos valores em cada posição correspondente.

Exercício 3: Caminho da Raiz até uma Folha

Implemente um método que encontre e imprima todos os caminhos da raiz até cada folha da árvore. Para a árvore do exemplo principal, a saída deve ser:

1 -> 2 -> 4
1 -> 2 -> 5
1 -> 3 -> 6

Conclusão

A árvore binária é o alicerce sobre o qual muitas outras estruturas de dados são construídas. Neste artigo, vimos como implementá-la em Rust usando enum e Box, aproveitando o pattern matching para escrever código elegante e seguro. As quatro formas de travessia — pré-ordem, em ordem, pós-ordem e em nível — são ferramentas essenciais que aparecem constantemente em algoritmos e entrevistas técnicas.

O sistema de tipos de Rust torna a implementação de árvores particularmente interessante: o enum com variantes Vazia e No elimina a necessidade de ponteiros nulos, e o Box garante que a memória é liberada corretamente quando a árvore sai de escopo. Nos próximos artigos, veremos como adicionar propriedades de ordenação (BST) e balanceamento (AVL, rubro-negra) a essa estrutura fundamental.