Árvores AVL
Aprenda sobre árvores AVL, balanceamento automático e suas operações (inserção, remoção e rotações).
Definição
Uma árvore AVL é uma árvore binária de busca auto-balanceada onde a diferença entre as alturas das subárvores esquerda e direita (fator de balanceamento) de qualquer nó não pode ser maior que 1. Esta propriedade garante que a árvore permaneça balanceada, mantendo operações com complexidade O(log n).
O nome AVL vem dos matemáticos Adelson-Velsky e Landis, que inventaram esta estrutura em 1962.
Propriedades Fundamentais:
- Fator de Balanceamento: Para cada nó, |altura(esquerda) - altura(direita)| ≤ 1
- Auto-balanceamento: Rotações automáticas mantêm o balanceamento após inserções/remoções
- Complexidade: Todas as operações têm complexidade O(log n) no pior caso
Abaixo temos um bloco de código em C que implementa uma árvore AVL completa com inserção, remoção e rotações:
#include <stdio.h>
#include <stdlib.h>
// Estrutura do nó da árvore AVL
typedef struct No {
int chave;
struct No* esquerda;
struct No* direita;
int altura;
} No;
// Função para obter a altura de um nó
int obterAltura(No* no) {
if (no == NULL)
return 0;
return no->altura;
}
// Função para calcular o máximo entre dois valores
int maximo(int a, int b) {
return (a > b) ? a : b;
}
// Função para criar um novo nó
No* criarNo(int chave) {
No* no = (No*)malloc(sizeof(No));
no->chave = chave;
no->esquerda = NULL;
no->direita = NULL;
no->altura = 1; // Novo nó é inicialmente inserido na folha
return no;
}
// Função para calcular o fator de balanceamento de um nó
int obterFatorBalanceamento(No* no) {
if (no == NULL)
return 0;
return obterAltura(no->esquerda) - obterAltura(no->direita);
}
// Rotação à direita
No* rotacaoDireita(No* y) {
No* x = y->esquerda;
No* T2 = x->direita;
// Realizar rotação
x->direita = y;
y->esquerda = T2;
// Atualizar alturas
y->altura = maximo(obterAltura(y->esquerda), obterAltura(y->direita)) + 1;
x->altura = maximo(obterAltura(x->esquerda), obterAltura(x->direita)) + 1;
// Retornar nova raiz
return x;
}
// Rotação à esquerda
No* rotacaoEsquerda(No* x) {
No* y = x->direita;
No* T2 = y->esquerda;
// Realizar rotação
y->esquerda = x;
x->direita = T2;
// Atualizar alturas
x->altura = maximo(obterAltura(x->esquerda), obterAltura(x->direita)) + 1;
y->altura = maximo(obterAltura(y->esquerda), obterAltura(y->direita)) + 1;
// Retornar nova raiz
return y;
}
// Função para inserir um nó na árvore AVL
No* inserir(No* no, int chave) {
// 1. Realizar inserção normal de BST
if (no == NULL)
return criarNo(chave);
if (chave < no->chave)
no->esquerda = inserir(no->esquerda, chave);
else if (chave > no->chave)
no->direita = inserir(no->direita, chave);
else // Chaves iguais não são permitidas
return no;
// 2. Atualizar altura do nó ancestral
no->altura = 1 + maximo(obterAltura(no->esquerda), obterAltura(no->direita));
// 3. Obter o fator de balanceamento deste nó ancestral
int balanceamento = obterFatorBalanceamento(no);
// Se este nó ficou desbalanceado, então temos 4 casos:
// Caso Esquerda Esquerda
if (balanceamento > 1 && chave < no->esquerda->chave)
return rotacaoDireita(no);
// Caso Direita Direita
if (balanceamento < -1 && chave > no->direita->chave)
return rotacaoEsquerda(no);
// Caso Esquerda Direita
if (balanceamento > 1 && chave > no->esquerda->chave) {
no->esquerda = rotacaoEsquerda(no->esquerda);
return rotacaoDireita(no);
}
// Caso Direita Esquerda
if (balanceamento < -1 && chave < no->direita->chave) {
no->direita = rotacaoDireita(no->direita);
return rotacaoEsquerda(no);
}
// Retornar o nó (inalterado)
return no;
}
// Função para encontrar o nó com valor mínimo
No* encontrarMinimo(No* no) {
No* atual = no;
while (atual->esquerda != NULL)
atual = atual->esquerda;
return atual;
}
// Função para remover um nó da árvore AVL
No* remover(No* raiz, int chave) {
// 1. Realizar remoção normal de BST
if (raiz == NULL)
return raiz;
if (chave < raiz->chave)
raiz->esquerda = remover(raiz->esquerda, chave);
else if (chave > raiz->chave)
raiz->direita = remover(raiz->direita, chave);
else {
// Este é o nó a ser removido
if (raiz->esquerda == NULL) {
// Nó sem filho esquerdo ou folha
No* temp = raiz->direita;
free(raiz);
return temp;
}
else if (raiz->direita == NULL) {
// Nó sem filho direito
No* temp = raiz->esquerda;
free(raiz);
return temp;
}
else {
// Nó com dois filhos: obter o sucessor inorder
No* temp = encontrarMinimo(raiz->direita);
// Copiar a chave do sucessor inorder para este nó
raiz->chave = temp->chave;
// Remover o sucessor inorder
raiz->direita = remover(raiz->direita, temp->chave);
}
}
// Se a árvore tinha apenas um nó, então retornar
if (raiz == NULL)
return raiz;
// 2. Atualizar altura do nó atual
raiz->altura = 1 + maximo(obterAltura(raiz->esquerda), obterAltura(raiz->direita));
// 3. Obter o fator de balanceamento deste nó
int balanceamento = obterFatorBalanceamento(raiz);
// Se este nó ficou desbalanceado, então temos 4 casos:
// Caso Esquerda Esquerda
if (balanceamento > 1 && obterFatorBalanceamento(raiz->esquerda) >= 0)
return rotacaoDireita(raiz);
// Caso Esquerda Direita
if (balanceamento > 1 && obterFatorBalanceamento(raiz->esquerda) < 0) {
raiz->esquerda = rotacaoEsquerda(raiz->esquerda);
return rotacaoDireita(raiz);
}
// Caso Direita Direita
if (balanceamento < -1 && obterFatorBalanceamento(raiz->direita) <= 0)
return rotacaoEsquerda(raiz);
// Caso Direita Esquerda
if (balanceamento < -1 && obterFatorBalanceamento(raiz->direita) > 0) {
raiz->direita = rotacaoDireita(raiz->direita);
return rotacaoEsquerda(raiz);
}
return raiz;
}
// Função para imprimir a árvore em ordem
void imprimirEmOrdem(No* raiz) {
if (raiz != NULL) {
imprimirEmOrdem(raiz->esquerda);
printf("%d ", raiz->chave);
imprimirEmOrdem(raiz->direita);
}
}
// Função para buscar um valor na árvore
No* buscar(No* raiz, int chave) {
if (raiz == NULL || raiz->chave == chave)
return raiz;
if (chave < raiz->chave)
return buscar(raiz->esquerda, chave);
return buscar(raiz->direita, chave);
}
// Função para imprimir informações detalhadas de um nó
void imprimirDetalhesNo(No* no) {
if (no != NULL) {
printf("Nó: %d, Altura: %d, Fator de Balanceamento: %d\n",
no->chave, no->altura, obterFatorBalanceamento(no));
}
}
// Função para liberar toda a árvore
void liberarArvore(No* raiz) {
if (raiz != NULL) {
liberarArvore(raiz->esquerda);
liberarArvore(raiz->direita);
free(raiz);
}
}
// Função para imprimir a árvore de forma visual (pré-ordem com indentação)
void imprimirArvoreVisual(No* raiz, int nivel) {
if (raiz != NULL) {
for (int i = 0; i < nivel; i++) {
printf(" ");
}
printf("%d (h:%d, fb:%d)\n", raiz->chave, raiz->altura, obterFatorBalanceamento(raiz));
if (raiz->esquerda != NULL || raiz->direita != NULL) {
imprimirArvoreVisual(raiz->esquerda, nivel + 1);
imprimirArvoreVisual(raiz->direita, nivel + 1);
}
}
}Agora, vamos criar uma função main para demonstrar o funcionamento da árvore AVL:
// Função principal
int main() {
No* raiz = NULL;
printf("=== Demonstração da Árvore AVL ===\n\n");
// Inserindo elementos
printf("Inserindo elementos: 10, 20, 30, 40, 50, 25\n");
raiz = inserir(raiz, 10);
raiz = inserir(raiz, 20);
raiz = inserir(raiz, 30);
raiz = inserir(raiz, 40);
raiz = inserir(raiz, 50);
raiz = inserir(raiz, 25);
printf("Árvore AVL em ordem: ");
imprimirEmOrdem(raiz);
printf("\n\n");
// Visualização da estrutura da árvore
printf("Estrutura da árvore (h=altura, fb=fator de balanceamento):\n");
imprimirArvoreVisual(raiz, 0);
printf("\n");
// Mostrando detalhes da raiz
printf("Detalhes da raiz:\n");
imprimirDetalhesNo(raiz);
printf("\n");
// Buscando um elemento
printf("Buscando o elemento 30: ");
No* resultado = buscar(raiz, 30);
if (resultado != NULL)
printf("Encontrado!\n");
else
printf("Não encontrado!\n");
// Removendo um elemento
printf("\nRemoção do elemento 10:\n");
raiz = remover(raiz, 10);
printf("Árvore AVL em ordem após remoção: ");
imprimirEmOrdem(raiz);
printf("\n\n");
printf("Estrutura da árvore após remoção:\n");
imprimirArvoreVisual(raiz, 0);
printf("\n");
printf("Detalhes da nova raiz:\n");
imprimirDetalhesNo(raiz);
// Teste adicional: removendo mais elementos
printf("\n=== Teste adicional de remoção ===\n");
printf("Removendo elemento 30:\n");
raiz = remover(raiz, 30);
printf("Árvore após remoção do 30: ");
imprimirEmOrdem(raiz);
printf("\n");
printf("Estrutura final:\n");
imprimirArvoreVisual(raiz, 0);
// Liberar memória
liberarArvore(raiz);
printf("\nMemória liberada com sucesso!\n");
return 0;
}Tipos de Rotações
As árvores AVL utilizam quatro tipos de rotações para manter o balanceamento:
1. Rotação Simples à Direita (LL)
Usada quando o fator de balanceamento > 1 e a inserção foi feita na subárvore esquerda da subárvore esquerda.
2. Rotação Simples à Esquerda (RR)
Usada quando o fator de balanceamento < -1 e a inserção foi feita na subárvore direita da subárvore direita.
3. Rotação Dupla Esquerda-Direita (LR)
Usada quando o fator de balanceamento > 1 e a inserção foi feita na subárvore direita da subárvore esquerda.
4. Rotação Dupla Direita-Esquerda (RL)
Usada quando o fator de balanceamento < -1 e a inserção foi feita na subárvore esquerda da subárvore direita.
Exemplos de contexto de uso:
Em que situações as árvores AVL são utilizadas?
- Sistemas de banco de dados:
- Árvores AVL são utilizadas em índices de banco de dados onde é necessário garantir tempos de busca consistentes e rápidos.
- Compiladores e interpretadores:
- Utilizadas em tabelas de símbolos para manter identificadores ordenados com acesso eficiente.
- Sistemas de arquivos:
- Alguns sistemas de arquivos utilizam árvores AVL para organizar metadados e diretórios de forma eficiente.
- Aplicações de tempo real:
- Quando é necessário garantir tempo de resposta previsível, as árvores AVL oferecem operações O(log n) no pior caso.
- Bibliotecas de containers:
- Muitas implementações de sets e maps em bibliotecas padrão utilizam árvores AVL ou estruturas similares.
Complexidade das Operações
| Operação | Complexidade Temporal | Complexidade Espacial |
|---|---|---|
| Busca | O(log n) | O(log n) recursiva |
| Inserção | O(log n) | O(log n) recursiva |
| Remoção | O(log n) | O(log n) recursiva |
| Traversal | O(n) | O(log n) recursiva |
As árvores AVL garantem que a altura da árvore seja sempre O(log n), assegurando que todas as operações fundamentais sejam eficientes mesmo no pior caso.