Árvore AVL
Nomeada pelos criadores Adelson-Velskii e Landis, é uma árvore de busca binária onde a complexidade de busca é O(log n) para n elementos.
É uma árvore balanceada por altura onde a diferença da altura das duas subárvores tenha modulo até 1.
Será considerado a seguinte estrutura para o nó:
struct nodeAVL {
nodeData data;
int fb, altura;
struct nodeAVL *pai, *esq, *dir;
};
Balanceamento
O fator de balanceamento e um nó é a altura da subárvore da esquerda - altura da subárvore da direita (pode ser o contrário)
O nó é considerado desbalanceado quando o fator de balanceamento tem módulo maior que 1.
Manutenção do balanceamento
Sempre que é realizada um alteração em um nó, o nó afetado, ou os ascendentes podem sofrer alteração do fator de balanceamento. Quando o fb ultrapassa o limite, é necessário realizar uma ou duas operação(ões) de rotação para balancear o nó.
Rotação
Quando um nó está desbalanceando em uma direção e o seu filho dessa direção estiver desbalanceando na mesma, é realizada uma rotação simples onde o nó é rotacionado na direção contrária a qual está desbalanceando. Se o filho estiver desbalanceando na direção contraria,é preciso realizar uma rotação no filho na direção oposta antes de realizar no pai.
Exemplo de rotação para a esquerda: A árvore a seguir tem o nó A com fator de balanceamento -2, e o seu filho da direita está desbalanceando para o mesmo lado. (os nós marcados como possível não são considerados, estão apenas para ilustrar o que acontece com o filho de C).
graph
A --- B[possível filho de A]
A --- C
C --- E[possível filho de C]
C --- D
Após realizar o rotação terá a seguinte forma:
graph
C --- A
A --- B[possível filho de A]
A --- E[possível filho de C]
C --- D
Exemplo em C:
- Para realizar a rotação para a direção oposta, é preciso trocar onde está esquerda por direita e vice versa.
- As funções
removeNodeFromPai
eupdatePaiNode
, servem para retirar e colocar um ponteiro para para o nó em seu pai. A funçãoatualizaBalanceamento
atualiza a altura e o fator de balanceamento do nó.
void LL(nodeAVL* node) {
nodeAVL* filhoDireita = node->dir;
removeNodeFromPai(node);
//pai de node vira pai de dir
filhoDireita->pai = node->pai;
updatePaiNode(filhoDireita);
//troca filho da esquerda de filhoDireita com filho da direita de node
node->dir = filhoDireita->esq;
if (node->dir)
node->dir->pai = node;
//filhoDireita vira pai de node
filhoDireita->esq = node;
filhoDireita->esq->pai = filhoDireita;
atualizaBalanceamento(filhoDireita);
atualizaBalanceamento(node);
}
Inserção
Para realizar a inserção, é simples, basta inserir o nó na árvore como em qualquer arvore binária. Ápos a inserção é preciso atualizar o fator de balanceamento do nó inserido e de seus ascendentes.
Exemplo em c:
- O código a seguir cuida do caso de inserir o nó raiz através do ponteiro para o ponteiro da raiz, e ignora elementos repetidos.
- A função
mantemBalanceamento
atualiza o balanceamento dos nós até chegar na raiz, e se necessário realiza o balanceamento. - O balanceamento pode alterar a raiz, nesse caso o pai do nó que era a raiz é a nova raiz.
nodeAVL* insertNode(nodeAVL** rootPtr, nodeData data) {
nodeAVL* root = *rootPtr;
if (root != NULL) {
nodeAVL* pai = searchPos(root, data);
bool alreadyExists = pai->data == data;
if (alreadyExists)
return pai;
nodeAVL* newNode = aloca();
newNode->data = data;
newNode->pai = pai;
updatePaiNode(newNode);
mantemBalanceamento(newNode);
//atualiza raiz
if (root->pai != NULL)
*rootPtr = root = root->pai;
return newNode;
} else {
*rootPtr = aloca();
(*rootPtr)->data = data;
return *rootPtr;
}
}
Remoção
A remoção é um pouco mais complicada, é preciso lidar com alguns casos. Se o nó removido possuir nenhum ou um filho, esse entra em seu lugar. Se o nó possuir dois filhos, é preciso encontrar o maior elemento da sua subárvore da esquerda, seu antecessor, e colocá-lo no lugar do nó removido.
Exemplo em c:
A primeira parte do código encontra o elemento a ser removido, se esse existir, se não ignora. Também verifica se existem filhos no nó, e remove o nó da árvore.
void removeNode(nodeAVL** rootPtr, nodeData data) {
nodeAVL* const root = *rootPtr;
if (root == NULL) return;
nodeAVL *toRemove = search(root, data),
*substituto = NULL;
if (toRemove == NULL)
return;
bool hasEsq = toRemove->esq != NULL,
hasDir = toRemove->dir != NULL,
isLeaf = !hasEsq && !hasDir;
removeNodeFromPai(toRemove);
A proxima parte lida com o caso de remover a folha e caso remova a raiz:
if (isLeaf) {
if (toRemove == root)
*rootPtr = NULL;
else if (toRemove->pai)
mantemBalanceamento(toRemove->pai);
}
As próximas duas partes lidam com os casos de remover quando há um ou dois filho(s).
Quando há dois, é utilizado o antecessor. O antecessor é retirado da árvore, e se possuir um filho, este vira filho de seu avô. Após obter o antecessor, o nó é removido da árvore e seus filhos são inseridos em seu antecessor.
else if (hasEsq && hasDir) {
substituto = antecessor(toRemove);
nodeAVL *paiDoAntecessor = substituto->pai,
*filhoAntecessor = substituto->esq;
removeNodeFromPai(substituto);
if (filhoAntecessor != NULL) {
filhoAntecessor->pai = paiDoAntecessor;
updatePaiNode(filhoAntecessor);
}
substituto->pai = toRemove->pai;
updatePaiNode(substituto);
substituto->dir = toRemove->dir;
substituto->esq = toRemove->esq;
substituto->dir->pai = substituto;
if (substituto->esq) //caso o antecessor seja filho do nó
substituto->esq->pai = substituto;
mantemBalanceamento(paiDoAntecessor);
}
No caso de haver apenas um filho, esse vira filho do pai do nó removido.
else {
if (hasEsq)
substituto = toRemove->esq;
else
substituto = toRemove->dir;
removeNodeFromPai(substituto);
substituto->pai = toRemove->pai;
updatePaiNode(substituto);
mantemBalanceamento(substituto);
}
Por fim a função verifica se uma duas partes acima alteraram a raiz e desaloca o nó
bool substitutoIsNewRoot = substituto != NULL && substituto->pai == NULL;
if (substitutoIsNewRoot)
*rootPtr = substituto;
desaloca(toRemove);
}
implementação
Segue a implementação completa: