Árvores
Autor(es): Jairo Litman
Até agora, exploramos estruturas de dados lineares (listas, pilhas, filas), onde os elementos são organizados em uma sequência. Agora, vamos entrar no mundo das estruturas de dados não-lineares, começando pelas árvores.
Árvores são estruturas de dados hierárquicas. Pense em uma árvore genealógica, na estrutura de pastas e arquivos do seu computador, ou no organograma de uma empresa. Todas essas são árvores.
Elas são incrivelmente eficientes para armazenar dados que precisam ser pesquisados ou mantidos em ordem. Por exemplo, uma Árvore Binária de Busca (BST) permite inserção e busca de dados muito mais rapidamente (em média) do que uma lista encadeada
Este capítulo focará nos fundamentos das árvores, com ênfase especial em Árvores Binárias. Abordaremos:
essencial (raiz, nó, folha, aresta, pai, filho, sub-árvore).
dois filhos (esquerdo e direito).
binária, que mantém os elementos ordenados para permitir buscas eficientes.
uma árvore: Pré-ordem (Pre-order), Em-ordem (In-order) e Pós-ordem (Post-order).
Binária de Busca em C.
Uma árvore é uma estrutura de dados não-linear que simula uma hierarquia. Ela consiste em um conjunto de elementos chamados nós (nodes) conectados por arestas (edges).
Terminologia de Árvores Para entender as árvores, precisamos de um vocabulário comum:
tem exatamente uma raiz.
Uma Árvore Binária é um tipo especial de árvore onde cada nó pode ter no máximo dois filhos: um filho à esquerda e um filho à direita.
Uma Árvore Binária de Busca (BST) (Binary Search Tree) é uma árvore binária que obedece a uma propriedade específica:
Para qualquer nó N :
Essa propriedade torna a busca por elementos muito eficiente. Se você está procurando o número 25 e o nó atual é 30, você sabe que (se 25 existir) ele deve estar na sub-árvore esquerda.
“Percorrer” uma árvore significa visitar cada nó exatamente uma vez. Existem três formas principais de fazer isso em profundidade (Depth-First Search):
último a sub-árvore Direita. (Raiz, Esquerda, Direita)
a sub-árvore Direita. (Esquerda, Raiz, Direita)
e por último o nó Raiz. (Esquerda, Direita, Raiz)
Vamos focar na implementação de uma Árvore Binária de Busca (BST) em C, pois ela é uma das mais úteis.
Assim como na lista encadeada, começamos definindo a estrutura do nó. Em uma árvore binária, cada nó precisa de ponteiros para a esquerda e para a direita.
typedef struct Node {
int data;
struct Node *left; // Ponteiro para o filho esquerdo
struct Node *right; // Ponteiro para o filho direito
} Node;A “árvore” em si é apenas um ponteiro para o nó raiz, que inicializamos como NULL.
Node *root = NULL;Precisamos de uma função auxiliar para criar (alocar) um novo nó, similar ao que fizemos em listas encadeadas.
Node* createNode(int data) {
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
// Tratar erro de alocação
return NULL;
}
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}A inserção em uma BST é uma operação recursiva. Começamos pela raiz e decidimos se o novo nó vai para a esquerda ou direita, com base na propriedade da BST.
Node* insertNode(Node *node, int data) {
// 1. Caso base: Se a árvore/sub-árvore está vazia,
// cria o novo nó e o retorna (ele se torna a nova raiz)
if (node == NULL) {
return createNode(data);
}
// 2. Caso recursivo: Decide para onde ir
if (data < node->data) {
// Vai para a esquerda
node->left = insertNode(node->left, data);
} else if (data > node->data) {
// Vai para a direita
node->right = insertNode(node->right, data);
}
// (Se data == node->data, não fazemos nada - não permitimos repetições)
// Retorna o ponteiro do nó (inalterado)
return node;
}
// Para usar:
// root = insertNode(root, 50);
// root = insertNode(root, 30);A pesquisa também é recursiva e tira proveito da ordenação da BST.
Node* search(Node *node, int data) {
// 1. Caso base 1: Nó não encontrado (chegou em NULL)
if (node == NULL) {
return NULL;
}
// 2. Caso base 2: Nó encontrado
if (node->data == data) {
return node;
}
// 3. Caso recursivo: Decide para onde ir
if (data < node->data) {
return search(node->left, data); // Busca na esquerda
} else {
return search(node->right, data); // Busca na direita
}
}Os percursos são implementações recursivas clássicas.
// Percurso Em-ordem (Esquerda, Raiz, Direita)
// Imprime os valores em ordem crescente
void inorderTraversal(Node *node) {
if (node == NULL) {
return;
}
inorderTraversal(node->left);
printf("%d ", node->data); // Visita a Raiz
inorderTraversal(node->right);
}
// Percurso Pré-ordem (Raiz, Esquerda, Direita)
void preorderTraversal(Node *node) {
if (node == NULL) {
return;
}
printf("%d ", node->data); // Visita a Raiz
preorderTraversal(node->left);
preorderTraversal(node->right);
}
// Percurso Pós-ordem (Esquerda, Direita, Raiz)
void postorderTraversal(Node *node) {
if (node == NULL) {
return;
}
postorderTraversal(node->left);
postorderTraversal(node->right);
printf("%d ", node->data); // Visita a Raiz
}Para liberar a memória de uma árvore, devemos usar um percurso Pós-ordem. Isso garante que liberamos os filhos (esquerda e direita) antes de liberar o pai (raiz).
void freeTree(Node *node) {
if (node == NULL) {
return;
}
// Libera sub-árvores filhas primeiro
freeTree(node->left);
freeTree(node->right);
// Libera o nó atual (Raiz)
free(node);
}