AVL

AVL é uma árvore binária de busca balanceada. A vantagem do balanceamento é garantir que a busca por um elemento vai ser efetuada em $O(lg(n))$ no pior caso.

Mas por que $lg(n)$???

Primeiramente, $lg$ equivale a $log$ na base 2.
Dado que a AVL é uma árvore balanceada, ela se assemelha muito a uma árvore completa.  

Uma árvore completa é uma árvore onde os elementos vão sendo adicionados de cima para baixo, da esquerda para a direita, dessa forma:


É possível notar que a altura dessa árvore vai ser sempre a menor possível.

• O nível 0 tem, no máximo, 1 elemento
• 
O nível 1 tem, no máximo, 2 elementos
• O nível 2 tem, no máximo, 4 elementos
                ...
• O nível tem, 
no máximo, $2^k$  elementos

Podemos deduzir que, numa árvore de k níveis, o número de nós será, no máximo $2^0 + 2^1 + 2^2 + ... + 2^k$ .

Observemos o seguinte:
$2^0$ em binário é: 0001
$2^1$ em binário é: 0010
$2^2$ em binário é: 0100
$2^3$ em binário é: 1000, etc... se somarmos tudo, obtemos 1111

Note que a próxima potência de 2 (isto é, $2^4$) é 10000, que é equivalente a 1111 + 1.

Logo, $2^0 + 2^1 + 2^2 + ... + 2^h = 2^{k+1} - 1$
Daí tiramos que o número de nós máximo até uma altura k é limitado por $2^{k+1} - 1}
[Obs: não provarei essa sentença porque o foco é na AVL, mas para provar, basta usar indução em k]

Dito isso, dado que a árvore tem n nós, como calcular a altura mínima que ela tem?

Basta tirar o $lg(n)$.

$lg(n)$ equivale à maior potência que podemos elevar o 2 para que ele seja menor ou igual a n. Se temos a potência k de 2, $lg(2^k) = k$.

Dado que temos n, que é o número de nós, chamemos a próxima potência de 2 maior que n de $2^k$. 
Sabemos que $2^k - 1$ é o maior número de nós possível de uma árvore de altura k - 1, ou seja, $2^k$ já tem um nível de altura a mais.
$lg(2^k) = k$, porém $lg(2^k - 1) < k$, então arredondamos o resultado para baixo, que resulta na altura correta de uma árvore com n nós!

Suponha uma árvore completa com 5 nós.
A próxima potência de 2 maior que n é $2^3$.
Sabemos que o máximo de nós que essa árvore pode ter é $2^3-1 = 7$.
$lg(2^3) = 3$, mas $lg(2^3 - 1) = lg(7) = 2$, assim como $lg(5) = 2$.

Sabemos como vai ser uma árvore completa com 5 nós, e sua altura de fato é 2.

Dito isso, o número de passos para se procurar um elemento numa árvore completa com 5 nós é igual à altura mais o acesso à raiz, ou seja, &lg(n) + 1$ operações, e $O(lg(n) + 1) = O(lg(n)).


Se a árvore tivesse 4 nós, seria equivalente, pois $lg(4) = 2 também.$.
Se a árvore tivesse 8 nós, a próxima potência de 2 seria 16, $lg(16-1) = 3$, um nível abaixo.

Então, no pior caso, o número de operações para se procurar um nó será 3 (como nos caminhos em verde), que é $lg(n) + 1$.

Agora vamos ao que interessa.

A AVL é uma estrutura semelhante à BST, mas sua estrutura leva em conta uma coisa a mais, o fator de balanceamento (fb). Este é o resultado da diferença entre as alturas da subárvore da esquerda e a subárvore da direita.

Na AVL, a altura é contada de baixo para cima, ou seja, o nó mais alto é a raiz.

Podemos definir a altura de um nó n como:

int H(no* n){
    if(n == NULL) return -1;
    else return 1 + max(H(n->left), H(n->right));
}
Isto é, -1 caso o nó não exista e 0 se o nó for folha. Caso contrário, sua altura será 1 mais a altura da maior das subárvores. 






Para uma árvore ser AVL, o fb de todos os seus nós deve estar contido em {-1, 0, 1}. Caso contrário, árvore está desbalanceada e não é AVL.

Dito isso, podemos representar um nó assim:

struct no{
    int val, altura;
    no* left, *right;
    no(){}
    no(int v){val = v, altura = 0, left = right = NULL;}
}; 
altura inicia 0, pois novos nós são inicialmente folhas.


Eis dois exemplos de árvores:
 
valor dentro do nó indica sua altura, ao lado, seu fb

Podemos ver que a árvore à direita tem um nó cujo fb não está em {-1, 0, 1}, logo essa árvore não é AVL.




INSERÇÃO



A inserção na AVL se faz da quase da mesma forma que numa BST comum, porém com uma diferença.

Pergunta: O que a AVL faz quando a árvore fica desbalanceada após sucessivas inserções?
Resposta: Ela faz rotações.


Como ela está sempre balanceada, quando ocorre uma inserção que o fb de um nó fica 2 ou -2, ela já se corrige. 
Não devemos nos preocupar em tratar casos como esse:


Se a árvore for AVL, nunca chegaremos em casos onde o fb de algum nó seja maior que 2 ou menor que -2, e se for 2 ou -2, será necessário fazer alguma rotação. 

Existem 4 casos de desbalanceamento:

[1] Esquerda - Esquerda
        - fb do nó desbalanceado e do seu filho mais pesado são positivos.
[2] Esquerda - Direita 
        fb do nó desbalanceado é positivo e seu filho mais pesado, negativo.
[3] Direita - Direita
        fb do nó desbalanceado e do seu filho mais pesado são negativos.
[4] Direita - Esquerda
        fb do nó desbalanceado é negativo e seu filho mais pesado, positivo.

Para reconhecer os casos, vemos assim:


As soluções são:
[1] Rotação Simples à Direita.
[2] Rotação Dupla à Direita. [RSE no filho + RSD no pai]
[3] Rotação Simples à Esquerda.
[4] Rotação Dupla à Esquerda. [RSD no filho + RSE no pai]

Exemplos:
Inserção de 8, 6 e 5 nessa ordem:

Árvore desbalanceou de forma Esquerda - Esquerda, então aplicamos RSD.

Inserção de 8, 6 e 7 nessa ordem:

fb do nó desbalanceado é positivo, e do filho mais pesado (o da esquerda) é negativo, logo é Esquerda-Direita, e devemos aplicar uma RDD.
RDD faz primeiro uma RSE no filho mais pesado do nó desbalanceado (o da esquerda), para que a árvore fique na configuração de receber uma RSD equivalente à de um desbalanceamento Esquerda - Esquerda.

Vejamos um exemplo mais complicado.
Suponha que temos essa árvore e adicionamos a ela o elemento 14:

A raiz da árvore ficou desbalanceada, mas é só checar qual dos 4 casos.
fb do nó desbalanceado é positivo, e do filho mais pesado (o da esquerda) é negativo, logo é Esquerda-Direita, e devemos aplicar uma RDD.

Primeiro aplicamos uma RSE no filho mais pesado (10) e depois uma RSD no nó desbalanceado (27):

Repare que, na RSE, o filho da esquerda do 15 vira filho da direita do 10, e o 10 vira filho da esquerda do 15. É assim sempre, apenas não era possível visualizar em casos mais simples.
Repare também que, na RSD, o filho da direita do 15 vira filha da esquerda do 27, e o 27 vira filho da direita do 15.

Resumindo, seja N um nó e e D seus filhos da esquerda e direita:

RSE em N
• Filho da esquerda de vira filho da direita de N;
N vira o filho da esquerda de D;

RSD em N:
• Filho da direita de vira filho da esquerda de N;
V vira o filho da direita de E;


Eis a implementação, por enquanto sem as rotações:

struct AVL{
    no *root;
    AVL(){root = NULL;}

    int H(no *n){return n == NULL? -1 : n->h;}

    int FB(no *n){return n == NULL? 0 : H(n->left) - H(n->right);}

    void add(int v){
        root = add(root, v);
    }

    no *add(no* n, int v){
        if(n == NULL) return new no(v);
        if(v >= n->val) n->right = add(n->right, v);
        else n->left = add(n->left, v);

        n->h = 1 + max(H(n->left), H(n->right));

        int fb = FB(n);
        //Esquerda
        if(fb > 1){
            //Esquerda
            if(v < n->left->val){
                return RSD(n);
            }
            //Direita
            else{
                n->left = RSE(n->left);
                return RSD(n);
            }
        }
        //Direita
        else if(fb < -1){
            //Direita
            if(v > n->right->val){
                return RSE(n);
            }
            //Esquerda
            else{
                n->right = RSD(n->right);
                return RSE(n);
            }
        }
        return n;
    }
};

Como a árvore desbalanceou no nó n e depois da inserção de v, podemos usar v para saber, entre os filhos do filho mais pesado de n, qual pesa mais.

Por exemplo, se fb > 1, sabemos que n está desbalanceado e que o filho mais pesado de n é o da esquerda.
Se v é menor que o valor do filho da esquerda de n, significa que o caso é Esquerda-Esquerda, então realizamos uma RSD em n.
Caso contrário, devemos efetuar RDD, isto é, uma RSE no filho mais pesado de n (o da esquerda) e uma RSD em n.


Rotação

Vamos relembrar as rotações simples:

Seja um nó e seus filhos da esquerda e direita:

RSE em N
• Filho da esquerda de vira filho da direita de N;
• vira o filho da esquerda de D;

RSD em N:
• Filho da direita de vira filho da esquerda de N;
• vira o filho da direita de E;



Como as duas funcionam essencialmente da mesma forma, vamos focar na RSE:


Para realizar essas duas operações, temos que guardar apontadores para:
• D
• Filho da esquerda de D


Podemos ver que o E não muda, só o que muda é N->right e D->left.

Se fizermos $D->left = N$, perdemos a referência para a subárvore C, então vamos salvá-la antes (digamos, em b). 
Dado que temos ela salva, fazemos $D->left = N$.

Estamos assim:


Agora devemos mudar o filho da direita de N para apontar para C, mas agora nossa referência para C é o b, então fazemos $N->right = b$.

Logo:



Traduzindo isso para código, temos:

no *RSE(no *N){
    no *D = N->right;
    no *b = D->left;

    D->left = N;
    N->right = b;

    //atualiza as alturas
    N->h = max(H(N->left), H(N->right)) + 1;
    D->h = max(H(D->left), H(D->right)) + 1;

    return D;
}

Analogamente:

no *RSD(no *N){
    no *E = N->left;
    no *b = E->right;

    E->right = N;
    N->left = b;

    //atualiza as alturas
    N->h = max(H(N->left), H(N->right))+1;
    E->h = max(H(E->left), H(E->right))+1;

    return E;
}

A atualização das alturas é feita em tempo constante porque a altura dos filhos dos nós atualizados não muda, apenas seus filhos viram outros, por isso não é necessário descer até as folhas, basta acessar o atributo de altura dos filhos.

Como, no pior caso, precisamos percorrer toda a altura da árvore para realizar a inserção e ainda realizar rotação dupla, a complexidade é $O(lg(n)) + O(RSE) + O(RSD)$, mas as rotações são $O(1)$, logo $O(lg(n)) + O(1) + O(1) = O(lg(n)) + 0 + 0 = O(lg(n))$.


 Complexidade da Inserção: O(lg(n))  



Em breve, remoção em AVL...

Nenhum comentário:

Postar um comentário