Árvore Binária

RESUMO

Uma árvore binária é uma estrutura de dados onde cada nó tem até dois filhos, usados para busca e ordenação eficientes. Na versão efêmera, alterações modificam diretamente a estrutura existente. Na versão persistente, cada modificação cria uma nova árvore compartilhando os nós não alterados, permitindo acesso a versões anteriores sem duplicação completa.

Uma árvore binária é uma estrutura de dados hierárquica composta por nós, onde cada nó tem no máximo dois filhos, geralmente referidos como filho esquerdo e filho direito. Essa estrutura é amplamente utilizada em diversas áreas da ciência da computação, incluindo algoritmos de busca, ordenação e manipulação de dados.

História e Origem

As árvores binárias têm suas raízes nos primeiros trabalhos sobre estruturas de dados e algoritmos. Elas foram formalmente descritas e estudadas em meados do século XX, com aplicações práticas e teóricas sendo amplamente exploradas por cientistas como John von Neumann e Claude Shannon. A simplicidade e eficiência das árvores binárias contribuíram para sua popularidade contínua.

Aplicações

Árvores binárias são utilizadas em várias aplicações, incluindo:

  • Árvores de Busca Binária (BST): Para operações eficientes de busca, inserção e exclusão de elementos.
  • Árvores AVL e Red-Black: Tipos de árvores binárias balanceadas que mantêm o equilíbrio para garantir operações de tempo logarítmico.
  • Árvores de Segmento: Utilizadas em problemas de intervalo.
  • Heap Binário: Utilizado em algoritmos de fila de prioridade e na implementação do algoritmo de Dijkstra.

Funcionamento

Estrutura Básica

Cada nó em uma árvore binária contém:

  • Valor: O dado armazenado no nó.
  • Filho Esquerdo: Referência ao nó filho à esquerda.
  • Filho Direito: Referência ao nó filho à direita.

A árvore começa com um nó raiz e, a partir daí, cada nó pode ter zero, um ou dois filhos.

Operações Básicas

  • Inserção: Adicionar um novo nó na árvore.
  • Busca: Encontrar um nó com um valor específico.
  • Exclusão: Remover um nó da árvore.

Complexidade

A complexidade das operações em uma árvore binária depende de sua altura h:

  • Melhor caso (árvore balanceada): O(log(n))
  • Pior caso (árvore degenerada em lista): O(n)

Implementação

Implementação Efêmera (Mutável)

Python
class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class BinaryTree:
    def __init__(self):
        self.root = None

    def insert(self, value):
        if self.root is None:
            self.root = Node(value)
        else:
            self._insert(self.root, value)

    def _insert(self, current_node, value):
        if value < current_node.value:
            if current_node.left is None:
                current_node.left = Node(value)
            else:
                self._insert(current_node.left, value)
        else:
            if current_node.right is None:
                current_node.right = Node(value)
            else:
                self._insert(current_node.right, value)

    def search(self, value):
        return self._search(self.root, value)

    def _search(self, current_node, value):
        if current_node is None:
            return False
        if value == current_node.value:
            return True
        elif value < current_node.value:
            return self._search(current_node.left, value)
        else:
            return self._search(current_node.right, value)

Implementação Persistente (Imutável)

Na implementação persistente, cada operação que modifica a árvore retorna uma nova versão da árvore, preservando as versões anteriores. Apenas os nós que foram alterados são recriados, enquanto os nós não afetados são reutilizados.

Python
class PersistentNode:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

class PersistentBinaryTree:
    def __init__(self, root=None):
        self.root = root

    def insert(self, value):
        new_root = self._insert(self.root, value)
        return PersistentBinaryTree(new_root)

    def _insert(self, node, value):
        if node is None:
            return PersistentNode(value)
        if value < node.value:
            new_left = self._insert(node.left, value)
            # Cria um novo nó com o filho esquerdo atualizado
            return PersistentNode(node.value, new_left, node.right)
        else:
            new_right = self._insert(node.right, value)
            # Cria um novo nó com o filho direito atualizado
            return PersistentNode(node.value, node.left, new_right)

    def search(self, value):
        return self._search(self.root, value)

    def _search(self, node, value):
        if node is None:
            return False
        if value == node.value:
            return True
        elif value < node.value:
            return self._search(node.left, value)
        else:
            return self._search(node.right, value)

Exemplo de Uso e Reaproveitamento de Nós

Vamos ilustrar com um exemplo, detalhando a criação e reutilização de nós:

Python
# Árvore original
pbt = PersistentBinaryTree()
pbt1 = pbt.insert(5)    # Cria o nó raiz 5
pbt2 = pbt1.insert(3)   # Cria o nó 3 e atualiza o nó raiz 5
pbt3 = pbt2.insert(7)   # Cria o nó 7 e atualiza o nó raiz 5

# Nova versão da árvore após inserção de 6
pbt4 = pbt3.insert(6)   # Cria o nó 6, atualiza o nó 7, e atualiza o nó raiz 5

# Verificações para demonstrar a criação e reutilização de nós
print(pbt3.root.value)           # Saída: 5
print(pbt4.root.value)           # Saída: 5 (novo nó raiz, mesmo valor)

print(pbt3.root.left.value)      # Saída: 3
print(pbt4.root.left.value)      # Saída: 3 (mesmo nó filho esquerdo, reutilizado)

print(pbt3.root.right.value)     # Saída: 7
print(pbt4.root.right.value)     # Saída: 7 (novo nó filho direito, mesmo valor)

print(pbt4.root.right.left.value)  # Saída: 6 (novo nó inserido)

Comparações

  • Efêmera (Mutável): Mais simples e eficiente em termos de memória para operações básicas. Alterações afetam a árvore original.
  • Persistente (Imutável): Mantém versões anteriores da árvore, permitindo retroceder a estados anteriores. Pode ser mais complexo e consumir mais memória, mas reaproveita nós para economizar espaço.

Melhores Práticas

  • Utilize árvores binárias balanceadas (como AVL ou Red-Black) para garantir desempenho logarítmico em operações de inserção, busca e exclusão.
  • Evite árvores binárias degeneradas (semelhantes a listas) através de balanceamento ou ao garantir diversidade nos dados de entrada.
  • Considere persistência se precisar de histórico de versões da estrutura de dados.

Referências

  • “Introduction to Algorithms” de Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, e Clifford Stein.
  • “Algorithms” de Robert Sedgewick e Kevin Wayne.
  • “Data Structures and Algorithms in Python” de Michael T. Goodrich, Roberto Tamassia, e Michael H. Goldwasser.

Conclusão

Árvores binárias são estruturas de dados fundamentais com diversas aplicações práticas. Implementações efêmeras são adequadas para a maioria dos casos de uso, enquanto implementações persistentes são úteis quando é necessário manter versões anteriores da estrutura. Entender e utilizar árvores binárias de forma eficaz é crucial para resolver problemas complexos de maneira eficiente.

Gostaria de mais informações?

Se você tem interesse neste assunto ou gostaria de mais informações sobre como a EximiaCo pode ajudar a sua empresa a utilizar a tecnologia para gerar mais resultados, entre em contato conosco.

0
Gostaríamos de ouvir sua opinião!x

Tenho interesse em conversar

Se você está querendo gerar mais resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Área de colaboradores

Esse ambiente é de acesso restrito à equipe de colaboradores da EximiaCo.

Trabalha na EximiaCo? Então conecte-se com sua conta: