Avançar para o conteúdo principal

Binary Tree e BST Explained

Árvore binária e BST explicada

Na computação, a árvore binária (ou binary tree, em inglês) é uma estrutura de dados hierárquica composta por nós, onde cada nó tem no máximo dois filhos: um filho à esquerda e um filho à direita. Esta estrutura é amplamente utilizada em várias áreas da ciência da computação, como organização de dados, algoritmos de busca, compressão de dados, entre outros.

Estrutura da Árvore Binária

Componentes da Árvore Binária

  • Nó (Node): Cada elemento da árvore é chamado de nó. Ele contém um valor ou dado e referências (ponteiros) para seus filhos.
  • Raiz (Root): O nó superior da árvore. É o ponto de entrada e não tem pai.
  • Filhos (Children): Nós que descendem de um outro nó. Cada nó pode ter até dois filhos.
  • Folhas (Leaves): Nós que não têm filhos.
  • Subárvore (Subtree): Qualquer nó com todos os seus descendentes forma uma subárvore.

Tipos de Árvores Binárias

  1. Árvore Binária Completa: Todos os níveis, exceto possivelmente o último, estão completamente preenchidos, e todos os nós estão o mais à esquerda possível.
  2. Árvore Binária Perfeita: Todos os nós internos têm exatamente dois filhos, e todas as folhas estão no mesmo nível.
  3. Árvore Binária Balanceada: A altura das subárvores esquerda e direita de qualquer nó diferem em no máximo uma unidade.
  4. Árvore Binária de Busca (BST - Binary Search Tree): Para cada nó, todos os valores na subárvore à esquerda são menores, e todos os valores na subárvore à direita são maiores.

Funcionamento de uma Árvore Binária de Busca (BST)

Uma BST é uma árvore binária que mantém uma propriedade específica: para cada nó, todos os valores na subárvore à esquerda são menores e todos os valores na subárvore à direita são maiores.

Operações em BST

  1. Inserção: Para inserir um novo valor, começa-se na raiz e compara-se o valor a ser inserido com o valor do nó atual. Se for menor, move-se para a subárvore à esquerda, se for maior, move-se para a subárvore à direita. Este processo continua até encontrar um nó folha onde o novo valor pode ser inserido.
  2. Busca: Similar à inserção, começa-se na raiz e compara-se o valor buscado com o valor do nó atual, movendo-se para a esquerda ou direita conforme necessário até encontrar o valor ou concluir que ele não está na árvore.
  3. Remoção: A remoção é mais complexa e varia dependendo do número de filhos do nó a ser removido (nenhum, um ou dois). O caso mais complicado é quando o nó tem dois filhos, onde se geralmente encontra-se o sucessor (o menor valor na subárvore à direita) para substituir o nó removido.

Desenho de uma BST

Aqui está um exemplo de uma BST:

        8
       / \
      3   10
     / \    \
    1   6    14
       / \   /
      4   7 13

Neste exemplo:

  • A raiz é 8.
  • O nó 3 é o filho esquerdo da raiz e 10 é o filho direito.
  • O nó 1 é o filho esquerdo de 3, e 6 é o filho direito.
  • O nó 4 é o filho esquerdo de 6 e 7 é o filho direito.
  • O nó 14 é o filho direito de 10 e 13 é o filho esquerdo de 14.

Implementação em Python

Aqui está um exemplo básico de implementação de uma BST em Python:

class Node:
    def __init__(self, key):
        self.left = None
        self.right = None
        self.val = key

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

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

    def _insert(self, root, key):
        if key < root.val:
            if root.left is None:
                root.left = Node(key)
            else:
                self._insert(root.left, key)
        else:
            if root.right is None:
                root.right = Node(key)
            else:
                self._insert(root.right, key)

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

    def _search(self, root, key):
        if root is None or root.val == key:
            return root
        if key < root.val:
            return self._search(root.left, key)
        else:
            return self._search(root.right, key)

# Exemplo de uso
bst = BST()
bst.insert(8)
bst.insert(3)
bst.insert(10)
bst.insert(1)
bst.insert(6)
bst.insert(4)
bst.insert(7)
bst.insert(14)
bst.insert(13)

# Busca
node = bst.search(6)
if node:
    print("Valor encontrado:", node.val)
else:
    print("Valor não encontrado.")

Neste exemplo, criamos uma classe Node para representar cada nó e uma classe BST para representar a árvore binária de busca. A árvore permite inserção e busca de valores, seguindo as regras mencionadas.

Árvore de Síntese Abstrata (AST) e Árvore de Análise (Parse Tree)

Em compiladores e linguagens de programação, as árvores de síntese abstrata (AST) e as árvores de análise (Parse Tree) são estruturas de dados essenciais usadas para representar a estrutura de programas de computador.

Árvore de Análise (Parse Tree)

A árvore de análise, também conhecida como árvore de derivação ou árvore de análise sintática, é uma representação detalhada de uma expressão conforme definida por uma gramática formal. Ela mostra como a entrada é derivada a partir das regras da gramática.

Características da Árvore de Análise

  • Nodos Internos: Representam produções da gramática.
  • Folhas: Representam tokens ou terminais da entrada.
  • Representação Completa: Captura todos os detalhes da gramática utilizada para derivar a entrada.

Exemplo de Árvore de Análise

Considere a gramática simples para expressões aritméticas:

E -> E + T | T
T -> T * F | F
F -> ( E ) | id

E a expressão de entrada: id + id * id

A árvore de análise para esta expressão seria:

        E
       / \
      E   +
     / \   \
    T   id  T
   / \      / \
  F   *    F   id
 / \      / \
id id    id id

Árvore de Síntese Abstrata (AST)

A árvore de síntese abstrata (AST) é uma representação simplificada da árvore de análise. Ela elimina muitos dos detalhes sintáticos da árvore de análise, focando na estrutura hierárquica e na essência semântica do código.

Características da AST

  • Nodos Internos: Representam operações ou construtos significativos da linguagem.
  • Folhas: Representam operandos ou identificadores.
  • Simplificação: Elimina símbolos intermediários e focos sintáticos desnecessários.

Exemplo de AST

Para a mesma expressão id + id * id, a AST seria:

      +
     / \
   id   *
        / \
      id  id

Comparação: Parse Tree vs AST

  • Detalhamento: A árvore de análise detalha cada passo de acordo com a gramática, enquanto a AST abstrai para capturar apenas a estrutura semântica.
  • Complexidade: A árvore de análise é mais complexa devido à inclusão de todos os símbolos da gramática, enquanto a AST é mais compacta e fácil de manipular.

Implementação Básica em Java

Exemplo de Classes para Parse Tree e AST

Aqui está um exemplo simplificado de como representar e construir uma árvore de análise e uma AST em Java:

// Nodo básico para árvores
class Node {
    String value;
    List<Node> children;

    Node(String value) {
        this.value = value;
        this.children = new ArrayList<>();
    }

    void addChild(Node child) {
        children.add(child);
    }
}

// Construção de uma Parse Tree
Node parseTreeExample() {
    Node root = new Node("E");
    Node e1 = new Node("E");
    Node plus = new Node("+");
    Node t1 = new Node("T");
    Node t2 = new Node("T");
    Node id1 = new Node("id");
    Node id2 = new Node("id");
    Node id3 = new Node("id");
    Node star = new Node("*");

    root.addChild(e1);
    root.addChild(plus);
    root.addChild(t2);
    e1.addChild(t1);
    t1.addChild(id1);
    t2.addChild(t1);
    t2.addChild(star);
    t2.addChild(id3);

    return root;
}

// Construção de uma AST
Node astExample() {
    Node root = new Node("+");
    Node id1 = new Node("id");
    Node star = new Node("*");
    Node id2 = new Node("id");
    Node id3 = new Node("id");

    root.addChild(id1);
    root.addChild(star);
    star.addChild(id2);
    star.addChild(id3);

    return root;
}

Visualização das Estruturas

Parse Tree

        E
       / \
      E   +
     / \   \
    T   id  T
   / \      / \
  F   *    F   id
 / \      / \
id id    id id

AST

      +
     / \
   id   *
        / \
      id  id

Aplicações de AST e Parse Tree

  • Parse Tree: Usada principalmente para análise sintática durante a fase de parsing em compiladores.
  • AST: Usada para análise semântica, otimização de código, e geração de código durante a compilação.

As árvores de análise e síntese abstrata são fundamentais para a construção de compiladores e interpretes, permitindo a representação estruturada e manipulável de programas de computador.


Árvore de Análise (Parse Tree) em C

A árvore de análise, ou árvore sintática, é uma representação detalhada de uma expressão conforme definida por uma gramática formal.

Exemplo de Gramática

Vamos considerar uma gramática simples para expressões aritméticas:

E -> E + T | T
T -> T * F | F
F -> ( E ) | id

E a expressão de entrada: id + id * id

Estrutura da Parse Tree

A estrutura da árvore de análise para essa expressão seria:

        E
       / \
      E   +
     / \   \
    T   id  T
   / \      / \
  F   *    F   id
 / \      / \
id id    id id

Implementação em C

Vamos implementar a construção de uma Parse Tree em C. Primeiro, definimos a estrutura do nodo e depois construímos a árvore para a expressão id + id * id.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Estrutura de um nodo na árvore de análise
typedef struct Node {
    char *value;
    struct Node **children;
    int child_count;
} Node;

// Função para criar um novo nodo
Node* createNode(char *value, int child_count) {
    Node *node = (Node *)malloc(sizeof(Node));
    node->value = strdup(value);
    node->child_count = child_count;
    if (child_count > 0) {
        node->children = (Node **)malloc(sizeof(Node *) * child_count);
    } else {
        node->children = NULL;
    }
    return node;
}

// Função para imprimir a árvore (recursiva)
void printTree(Node *root, int level) {
    if (root == NULL) return;
    for (int i = 0; i < level; i++) printf("  ");
    printf("%s\n", root->value);
    for (int i = 0; i < root->child_count; i++) {
        printTree(root->children[i], level + 1);
    }
}

// Função principal para construir a árvore de análise da expressão "id + id * id"
Node* buildParseTree() {
    Node *root = createNode("E", 3);
    
    Node *e1 = createNode("E", 1);
    Node *plus = createNode("+", 0);
    Node *t2 = createNode("T", 3);

    Node *t1 = createNode("T", 1);
    Node *id1 = createNode("id", 0);
    Node *id2 = createNode("id", 0);
    Node *id3 = createNode("id", 0);
    Node *star = createNode("*", 0);

    root->children[0] = e1;
    root->children[1] = plus;
    root->children[2] = t2;
    
    e1->children[0] = t1;
    t1->children[0] = id1;
    
    t2->children[0] = t1;
    t2->children[1] = star;
    t2->children[2] = id3;

    return root;
}

int main() {
    Node *parseTree = buildParseTree();
    printTree(parseTree, 0);
    return 0;
}

Árvore de Síntese Abstrata (AST) em C

A árvore de síntese abstrata (AST) é uma representação simplificada da árvore de análise que captura a estrutura hierárquica e a essência semântica do código.

Estrutura da AST

Para a expressão id + id * id, a AST seria:

      +
     / \
   id   *
        / \
      id  id

Implementação em C

Vamos implementar a construção de uma AST para a mesma expressão id + id * id.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Estrutura de um nodo na AST
typedef struct Node {
    char *value;
    struct Node *left;
    struct Node *right;
} Node;

// Função para criar um novo nodo
Node* createASTNode(char *value, Node *left, Node *right) {
    Node *node = (Node *)malloc(sizeof(Node));
    node->value = strdup(value);
    node->left = left;
    node->right = right;
    return node;
}

// Função para imprimir a árvore (recursiva)
void printAST(Node *root, int level) {
    if (root == NULL) return;
    for (int i = 0; i < level; i++) printf("  ");
    printf("%s\n", root->value);
    printAST(root->left, level + 1);
    printAST(root->right, level + 1);
}

// Função principal para construir a AST da expressão "id + id * id"
Node* buildAST() {
    Node *id1 = createASTNode("id", NULL, NULL);
    Node *id2 = createASTNode("id", NULL, NULL);
    Node *id3 = createASTNode("id", NULL, NULL);
    
    Node *star = createASTNode("*", id2, id3);
    Node *plus = createASTNode("+", id1, star);

    return plus;
}

int main() {
    Node *ast = buildAST();
    printAST(ast, 0);
    return 0;
}

Comparação: Parse Tree vs AST

  • Parse Tree: Captura todos os detalhes da gramática utilizada para derivar a entrada. É mais detalhada e complexa.
  • AST: Simplifica a estrutura, removendo símbolos intermediários e focando na essência semântica da expressão. É mais compacta e fácil de manipular.

Conclusão

As árvores de análise e síntese abstrata são fundamentais na construção de compiladores e intérpretes. A árvore de análise fornece uma representação detalhada da estrutura gramatical, enquanto a AST fornece uma representação simplificada e semântica do código. Em C, essas estruturas podem ser implementadas utilizando estruturas de dados adequadas e funções recursivas para construção e manipulação.

Comentários

Mensagens populares deste blogue

Sistema de Recomendação do Twitter

Sistema de Recomendação do Twitter O sistema de recomendação complexo do Twitter, onde múltiplos componentes trabalham em conjunto para selecionar e classificar os tweets que são exibidos para os usuários na aba "Para você". Vamos descrever cada componente e fase do processo, ilustrando com diagramas e exemplos de código onde aplicável. Fases do Sistema de Recomendação Candidate Sourcing Esta fase é responsável por selecionar os tweets candidatos que podem ser recomendados ao usuário. Os candidatos são coletados de diferentes fontes e algoritmos, como RealGraph, TweepCred, Trust & Safety, GraphJets, etc. Light Ranker (Earlybird) Depois de obter os candidatos, um modelo de machine learning leve (light ranker) faz uma primeira classificação desses tweets. Heavy Ranker Os tweets classificados pelo light ranker são então processados por um modelo mais pesado e complexo (heavy ranker) para uma classificação mais precisa. Heurísticas e Filtros Após o ranqueamento dos tweets, el...

Protocol Buffers (protobuf)

Protocol Buffers (protobuf) O Protocol Buffers (protobuf) é uma alternativa eficiente ao JSON para serialização de dados, especialmente em ambientes de comunicação entre sistemas ou micro-serviços. Aqui estão alguns pontos-chave que explicam por que o protobuf é preferido nesses cenários: Estrutura de Dados Definida No protobuf, você define a estrutura dos dados usando um arquivo .proto , como mostrado no exemplo da mensagem Product e Image . Isso especifica explicitamente cada campo e seu tipo de dados. message Product { string product_id = 1; string name = 2; string description = 3; float price = 4; bool availability = 5; repeated Image images = 6; } message Image { string url = 1; string type = 2; } Compactação de Dados Ao contrário do JSON, que é um formato de texto legível por humanos e verbose, o protobuf gera um formato binário compacto e eficiente. No exemplo mencionado, o mesmo conjunto de dados ocuparia menos de 80 bytes em formato protobuf, comparado a quas...

File Types: Differences

 Tipos de Arquivos: Diferenças As diferenças entre os tipos de arquivos como imagem, texto, áudio, vídeo, e arquivos de redes e mensageria podem ser entendidas considerando como os dados são estruturados, armazenados e processados em termos de bits e bytes. Vamos analisar cada tipo: Imagem Arquivos de imagem são matrizes de pixels, onde cada pixel é representado por um valor de cor. A profundidade de cor (número de bits por pixel) determina a quantidade de cores possíveis. Tipos comuns : JPEG, PNG, BMP, GIF. Estrutura : Matriz bidimensional de pixels. Grayscale (escala de cinza) : Cada pixel é geralmente representado por 8 bits (1 byte). RGB (colorido) : Cada pixel tem 3 componentes (vermelho, verde, azul), cada um representado por 8 bits, totalizando 24 bits (3 bytes) por pixel. PNG com transparência (RGBA) : 4 componentes (vermelho, verde, azul, alfa), totalizando 32 bits (4 bytes) por pixel. Texto Arquivos de texto armazenam caracteres em sequências de bytes. O número de bytes p...