Em Construção, com vi!!!

Data da última atualização: 13/03/2000
copyright Adriano Cruz 1999

Árvores

------------------------------------
  1. Introdução
  2. Definições Básicas
  3. Àrvores Binárias
  4. Armazenamento de Árvores Binárias
  5. Uma Aplicação de Árvores Binárias
  6. Percorrendo Árvores Binárias
  7. O Algoritmo de Huffman
  8. Removendo Nós de Árvores Binárias
  9. Árvores Binárias Balanceadas
  10. Exercícios
-----------------------------------------------------------

Introdução

 

  -----------------------------------------------------------

Definições Básicas


Árvores são estruturas de dados extremamente úteis em muitas aplicações. Uma árvore é formada por um conjunto finito T de elementos denominados vértices ou nós de tal modo que se T = 0 a árvore é vazia, caso contrário temos um nó especial chamado raiz da árvore (r), e cujos elementos restantes são particionados em m>=1 conjuntos distintos não vazios, as subárvores de r, sendo cada um destes conjuntos por sua vez uma árvore.

A forma convencional de representar uma árvore está indicado na figura aini abaixo. Esta árvore tem nove nós sendo A o nó raiz.


árvore binária

Figura (aini): Uma árvore



Os conjuntos das subárvores tem de ser disjuntos tem de ser disjuntos portanto a estrutura indicada na Figura arvn não é uma árvore.

Estrutura que não representa árvore

Figura arvn: Estrutura que não representa uma árvore



Se n é um nó da árvore T então Tn indica uma subárvore de T com raiz no nó n. Os nós n1, n2, ..., nk das subárvores de Tn são chamados de filhos de n e n é o pai destes nós, que são nós irmãos. Os nós B e C são filhos de A e nós irmãos. Nós sem filhos como os nós D, H, I, F e G são chamados de folhas. A subárvore da esquerda do nó A tem raiz em B e a subárvore da direita tem raiz em C, isto está indicado pelos dois ramos saindo de A. A ausência de um ramo na árvore indica uma subárvore vazia, como a subárvore da direita do nó B.  O número de de filhos de um nó é chamado de grau de saída deste nó. Por exemplo, o nó C tem grau de saída 3 e o nó E grau 2. Se o nó n é a raiz de uma subárvore Tn e n1 pertence a Tn então n1 é descendente de n e n ancestral de n1. Portanto nós sem descendentes próprios é uma folha. Por exemplo, o nó H é ancestral do nó C e o nó D é descendente do nó A.

Um caminho da árvore é composto por uma seqüência de nós consecutivos (n1, n2, ..., nk-1, nk) tal que existe sempre a relação ni é pai de ni+1. Os k vértices formam k-1 pares e um caminho de comprimento igual a k-1.  O comprimento do caminho entre o nó A e o nó H é 3.

O nível de um nó n pode ser definido do seguinte modo: o nó raiz tem nível 0, os outros nós tem um nível que é maior uma unidade que o nível de seu pai. Na árvore da figura anterior temos nós nos seguintes níveis:


A altura de um nó n é o número de nós do maior caminho de n até um de seus descendentes. As  folhas tem altura 1.
 

Existem diversas maneiras de representar árvores. Uma representação que reflete a idéia de árvores como conjuntos aninhados é mostrado na figura arvconj abaixo. A figura mostra o mesmo conjunto da figura aini.

Árvores como conjuntos aninhados

Figura (arconj): Árvore representada como conjuntos aninhados.

Uma outra notação que encontramos a toda hora, e que está representada na figura arviden, é a forma identada ou de diagrama de barras. Notar que esta representação lembra um sumário de livro. Os sumários  dos livros são representações da árvore do conteúdo do livro.
 

Árvore representada por identação
Figura (arviden) Árvore e sua representação por barras



Uma outra forma interessante de representar uma árvore é a representação por parênteses aninhados. Da mesma forma que a figura aini representa uma árvore no plano a representação por parênteses representa uma árvore em uma linha. A seqüência de parênteses representa a relação entre os nós da estrutura. O rótulo do nó é inserido à esquerda do abre parênteses correspondente. A árvore representada planarmente pela figura aini pode ser representada em uma linha por

(A (B(D))(C(E(H)(I))(F)(G)))

Esta representação tem importância, por exemplo, no tratamento de expressões aritméticas, já que toda expressão aritmética pode ser colocada nesta forma. Se colocarmos uma expressão nesta forma podemos então representá-la como uma árvore, mostrando como ela seria calculada. Para colocarmos uma expressão em forma de árvore devemos considerar cada operador como um nó da árvore e os seus operandos como as duas subárvores. Considere a expressão C seguinte

A + (B-C)*D%(E*F)

que após receber todos os parênteses fica da seguinte maneira

(A + ((B-C)*(D%(E*F))))



A figura arvexp mostra como fica esta expressão representada por uma árvore.
 

Expressão e sua representação como árvore

Figura (arvexp) Uma expressão e sua representação como árvore.

------------------------------------------------------------------------

Árvores Binárias

A figura arvbin abaixo mostra um importante tipo de árvore que é a árvore binária. Em uma árvore binária cada nó tem no máximo duas subárvores, e quando há somente uma presente é necessário distinguir entre subárvore esquerda e direita.  Árvores binárias podem ser vistas em diversas situações do cotidiano. Por exemplo, um torneio de futebol eliminatório, do tipo das copas dos países, como a Copa do Brasil, em que a cada etapa os times são agrupados dois a dois e sempre são eliminados metade dos times é uma árvore binária.

Figura abin: Árvore binária

Formalmente uma árvore binária pode ser definida como um conjunto finito de nós, que é vazio, ou consiste de um nó raiz e dois conjuntos disjuntos de nós, a subárvore esquerda e a subárvore direita. É importante observar que uma árvore binária não é um caso especial de árvore e sim um conceito completamente diferente. Por exemplo, considere a figura arvbind, note que são duas árvores idênticas, mas são duas árvores binárias diferentes. Isto porque uma delas tem a subárvore da direita vazia e a outra a subárvore da esquerda.

Árvores binárias diferentes

Figura arcbind: Árvores binárias diferentes.




Uma árvore estritamente binária é uma árvore binária em que cada nó tem 0 ou 2 filhos. Uma árvore binária cheia é uma árvore em que se um nó tem alguma sub-árvore vazia então ele está no último nível. Uma árvore completa é aquela em se n é um nó com algumas de subárvores vazias, então n se localiza no penúltimo ou no último nível. Portanto, toda árvore cheia é completa e estritamente binária. A Figura arvbcc mostra uma árvore estritamente binária, completa e cheia.


árvores estritamente binárias, completa e cheia
 

------------------------------------------------------------------------

Armazenamento de Árvores Binárias

Para armazenar cada nó de uma árvore binária precisamos de uma estrutura que contenha dois ponteiros: um aponta para a subárvore esquerda e outro para a subárvore direita. Naturalmente, devemos ter o(s) campo(s) para armazenar as informações que o nó deve conter. Nos algoritmos que iremos mostrar consideraremos que existe a seguinte definição para a estrutura do nó:

typedef struct sttNo {
    tipo inf;
    struct sttNo *esq, *dir;
} tNo;

A Figura armarv mostra um diagrama de como seria o armazenamento de uma árvore binária. Observar que se desconsiderarmos os campos de informação para armazenar uma árvore com n nós precisamos de 2n+1 unidades de memória.

Armazenamento em uma árvore binária




No processo de criar uma árvore precisaremos de três operações importantes: cria_arvore, pos_esq e pos_dir. cria_arvore cria uma árvore binária nova consistindo de um único nó, armazena a informação. e retorna um ponteiro para este nó. Um algoritmo para esta função pode ser o seguinte:

    p = cria_no();
    p->info = x;
    p->esq = NULL;
    p->dir = NULL;
    return p;

pos_esq aceita um ponteiro p para uma árvore binária sem filho esquerdo e cria um novo filho esquerdo contendo a informação x. Um possível algoritmo para esta função pode ser:

if (p->left)
    puts("Operação ilegal");
else {
    q = cria_arvore();
    p->left = q;
}

O algoritmo pos_dir é semelhante a este com a diferença que ele cria um nó a direita.

------------------------------------------------------------------------

Uma Aplicação de Árvores Binárias

As árvore binárias são estruturas importantes toda vez que uma decisão binária deve ser tomada em algum ponto de um algoritmo. Vamos agora, antes de passar a algoritmos mais complexos, mostrar uma aplicação simples de árvores binárias. Suponhamos que precisamos descobrir números duplicados em uma lista não ordenada de números. Uma maneira é comparar  cada novo número com todos os números já lidos. Isto aumenta em muito a complexidade do algoritmo. Outra possibilidade é manter uma lista ordenada dos números e a cada número lido fazer uma busca na lista. Outra solução é usar uma árvore binária para manter os números. O primeiro número lido é colocado na raiz da árvore. Cada novo número lido é comparado com o elemento raiz, caso seja igual é uma duplicata e voltamos a ler outro número. Se é menor repetimos o processo com a árvore da direita e se maior com a árvore da esquerda. Este processo continua até que uma duplicata é encontrada ou uma árvore vazia é achada. Neste caso, o número é inserido na posição devida na árvore.  Considere que os números

7 8 2 5 8 3 5 10 4

foram fornecidos pelo usuário, neste caso a árvore binária mostrada na Figura arbus seria construida.

Árvore de busca binária




O programa arv0300.c mostra este algoritmo. O programa insere os nós na árvore e imprime uma mensagem caso seja fornecido um número que já foi lido antes.

/* programa arv0300.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

typedef struct stNo {
  int info;
  struct stNo *esq, *dir;
} tNo ;

tNo *cria_arvore( int );
tNo *cria_no( );
void pos_esq (tNo *, int );
void pos_dir (tNo *, int );

void main() {
  tNo *raiz, *p, *q;
  char linha[80], *numero;
  int num;

  gets(linha);
  numero = strtok(linha, " "); /* pega o primeiro numero da lista */
  num = atoi(numero);
  raiz = cria_arvore(num); /* insere na raiz */
  numero = strtok(NULL, " ");
  while (numero) {
    q = raiz; p = raiz;
    printf("Li numero %d\n", num); /* le novo numero */
    num = atoi(numero);
    while (num != p->info && q) { /* procura na arvore */
      p = q;
      if (num < p->info)
 q = p->esq;               /* passa para arvore esquerda */
      else
 q = p->dir;               /* passa para direita */
    }
    if (num == p->info)
      printf("O numero %d ja existe na arvore.\n", num);
    else {  /* vou inserir o numero na arvore */
      if (num < p->info)
 pos_esq(p, num);
      else
 pos_dir(p, num);
    }
    numero = strtok(NULL, " ");
  } /* fim do while (numero) */
}

tNo *cria_arvore (int x) {
  tNo *p;

  p = cria_no ();
  if (p) {
    p->info = x;
    return p;
  }
  else {
    puts("Faltou espaco para alocar no.");
    exit(1);
  }
}

tNo *cria_no() {
  tNo *p;

  if ((p = (tNo *) malloc(sizeof(tNo))) == NULL)
    return NULL;
  else {
    p->esq = NULL; p->dir = NULL;
    return p;
  }
 }
 

void pos_esq(tNo *p, int x) {
  tNo *q;

  if (p->esq)
    puts("Operacao de insercao a esquerda ilegal.");
  else {
    q = cria_arvore(x);
    p->esq = q;
  }
}
 

void pos_dir(tNo *p, int x) {
  tNo *q;

  if (p->dir)
    puts("Operacao de insercao a direita ilegal.");
  else {
    q = cria_arvore(x);
    p->dir = q;
  }
}
 

------------------------------------------------------------------------

Percorrendo Árvores Binárias

Uma operação muito comum é percorrer uma árvore binária, o que significa passar por todos os nós, pelo menos uma vez. O conceito de visitar significa executar uma operação com a informação armazenada no nó, por exemplo, imprimir seu conteúdo. Na operação de percorrer a árvore pode-se passar por alguns nós mais de uma vez, sem porém visitá-los.

Uma árvore é uma estrutura não seqüêncial, diferentemente de uma lista, por exemplo. Não existe ordem natural para percorrer árvores e portanto podemos escolher diferentes maneiras de percorrê-las. Nós iremos estudar três métodos para percorrer árvores. Todos estes três métodos podem ser definidos recursivamente e se baseiam em três operações básicas: visitar a raiz, percorrer a subárvore da esquerda e percorrer a subárvore da direita. A única diferença entre estes métodos é a ordem em que estas operações são executadas.

O primeiro método, conhecido como percurso em pré-ordem, implica em executar recursivamente os três passos na seguinte ordem.
 

  1. Visitar a raiz;
  2. Percorrer a subárvore da esquerda em pré-ordem;
  3. Percorre a subárvore da direita em pré-ordem.
Para a árvore da Figura arvbinp este percurso forneceria, no caso da visita significar imprimir, os seguintes resultados: F B A D C E H G I. Uma aplicação interessante deste tipo de percurso é aplicá-lo à uma árvore que contenha uma expressão aritmética, a qual foi expandida e recebeu todos os parênteses. Por exemplo, aplicando-se o percurso em pré-ordem à árvore arvexp obtém-se como resultado a expressão em notação polonesa normal, isto é os operandos antes dos operadores. Deste modo o resultado é +A*-BC%D*EF. Observar que esta notação é diferente da notação polonesa reversa, em que os operadores aparecem depois dos operandos.

Um algoritmo recursivo para implementar este modo de percurso pode ser o seguinte:

void pre_ordem ( tipoNo *pt) {
    if (pt) {
        visita (pt);
        pre_ordem (pt->esq);
        pre_ordem (pt->dir);
    }
}

Árvore de busca binária

Para percorrer a árvore em ordem simétrica executa-se recursivamente os três passos na seguinte ordem:

  1. Percorrer a subárvore da esquerda em ordem simétrica;
  2. Visitar a raiz;
  3. Percorrer a subárvore da direita em ordem simétrica.
Um algoritmo recursivo para implementar este modo de percurso pode ser o seguinte:

void em_ordem ( tipoNo *pt) {
    if (pt) {
        em_ordem (pt->esq);
        visita (pt);
        em_ordem (pt->dir);
    }
}

Para a árvore da Figura arvbin o percurso forneceria o seguinte resultado A B C D E F G H I.

Este tipo de percurso é muito empregado em árvores binárias de busca. Considere a árvore mostrada na Figura arvbus, que foi gerada como está indicado na seção Uma Aplicação de Árvores Binárias. Caso a árvore seja percorrida em ordem simétrica o resultado seria

2 3 4 5 7 8 10

que são os números lidos em ordem crescente sem repetição.

O percurso conhecido como pós-ordem é feito a partir dos três passos na seguinte ordem:
 

  1. Percorrer a subárvore da esquerda em pós-ordem;
  2. Percorrer a subárvore da direita em pós-ordem;
  3. Visitar a raiz;
Para a árvore da Figura arvbin o percurso forneceria o seguinte resultado A C E D B G I H F. Um algoritmo recursivo para implementar este percurso poderia ser:

void pos_ordem(tipoNo *pt) {
    if (pt) {
        pos_ordem(pt->esq);
        pos_ordem(pt->dir);
        visita(pt);
}

O percurso em pós-ordem pode ser aplicado no cálculo da altura de uma árvore. Para calcular a altura de uma árvore é necessário calcular o maior caminho da raiz até uma de suas folhas. Deste modo só podemos calcular o comprimento de um caminho a partir de um nó v após percorrermos todos os seus descendentes. O algoritmo mostrado abaixo mostra como fica a implementação da função visita para que ela calcule a altura do nó.

void visita (tNo *p) {
     int alt1, alt2;
     if (p->esq) alt1 = p->esq->altura;
     else alt1 = 0;
     if (p->dir) alt2 = p->dir->altura;
     else alt2 = 0;
     if (alt1>alt2) p->altura = alt1 + 1;
     else p->altura = alt2 + 1;
     printf("info = %d ", p->info);
     printf("altura = %d\n", p->altura);
}

As variáveis alt1 e alt2 armazenam a altura das subárvores da esquerda e da direita e o campo altura é um novo campo da estrutura que armazena o nó. A altura de um nó é igual ao maior valor entre as alturas esquerda e direita incrementado de um.

------------------------------------------------------------------------

Algoritmo de Huffman

Para analisarmos mais uma aplicação de árvores binárias vamos considerar o problema de codificar uma mensagem composta de uma seqüência de símbolos de um alfabeto de n símbolos. Esta mensagem será transformada em uma seqüência de bits, depois de a cada símbolo for atribuído um código binário e os códigos dos símbolos da mensagem forem concatenados.

Considere um alfabeto composto de quatro símbolos A, B, C e D, sendo que a cada um dos símbolos foi atribuído o código indicado a seguir:
 
 

Símbolo Código
A 00
B 01
C 10
D 11
 

A mensagem ABCADCA seria codificada da seguinte maneira 00011000111000, tendo comprimento de 14 bits. O objetivo do algoritmo é criar um código que minimize o comprimento da mensagem. Para criar este código vamos levar em conta a freqüência de cada símbolo na mensagem. A Tabela a seguir mostra a freqüência de cada símbolo na mensagem
 
 

Símbolo Freqüência
A 3
B 1
C 2
D 1

Desta tabela podemos verificar que se atribuirmos ao símbolo A um código binário mais curto que os atribuídos aos símbolos B e D teríamos uma mensagem menor. Isto provém do fato que o símbolo A aparece mais vezes do que os símbolos B e D. Suponha que os seguintes códigos sejam atribuídos aos símbolos
 
 

Símbolo Código
A 0
B 110
C 10
D 111

Usando estge código a mensagem ABCADCA ficaria 0110100111100 que requer 13 bits. Em mensagens longas com mais símbolos infrequentes o ganho pode ser maior. Um dos requerimentos deste código é que nenhum código seja prefixo de outro, caso a decodificação seja feita da esquerda para direita.

Para decodificar a mensagem vamos começar da esquerda para a direita, caso o primeiro bit seja 0 o código corresponde ao símbolo A. No caso contrário devemos continuar a examinar os bits restantes. Se o segundo bit for 0 o símbolo é um C, caso contrário examinamos o terceiro bit, um 0 indica um B e D no outro caso.

Do que vomos até agora o algoritmo para encontrar o algoritmo ótimo é o seguinte. Encontre os dois símbolos que aparecem com menor freqüência,  no nosso caso B e D. Atribuímos 0 para B e 1 para D. Combine estes dois símbolos em um BD. Este novo símbolo terá freqüência igual a soma das freqüências de B e D, no caso 2. Temos agora os seguintes símbolos A (3), C (2) e BD (2), os números entre parênteses são as freqüências. Novamente devemos escolher os símbolos de menor freqüência, que são C e BD. Atribuímos o código 0 ao símbolo C e 1 ao BD. Isto siginifica adicionar 1 aos códigos de B e D, que passam a valer 10 e 11 respectivamente. Os dois símbolos são combinados então no símbolo CBD de freqüência 4. Temos agora dois símbolos A (3) e CBD (4). Atribuímos 0 ao símbolo A e 1 ao símbolo CBD. O símbolo ACBD é o único símbolo restante e recebe o código NULL de comprimento 0.  A Figura arvhuf1 mostra a árvore binária que pode ser construída a partir deste exemplo. Cada nó representa um símbolo e sua freqüência.
 

árvore de huffman para exemplo simples

Figura arvhuf1: Árvore de Huffman

Vamos considerar outro exemplo em que temos
 

Árvore de Huffman para exemplo maior




------------------------------------------------------------------------

Removendo Nós de Árvores Binárias

Para remover um nó de uma árvore binária devemos considerar três casos:
  1. nó sem filhos;
  2. nó com um unico filho;
  3. nó com dois filhos.
O  caso de um nó sem filhos é o mais simples e significa apenas ajustar o ponteiro de seu pai. A Figura remov0 ilustra este caso, onde o nó com o valor 8 é removido. No caso do nó ter um único filho a mudança na árvore também é simples significa mover o nó filho daquele será removido uma posição para cima como está ilustrado na Figura remove1, onde o nó com o valor 6 é removido. O caso mais complexo é o do nó com dois filhos. Neste caso devemos procurar o sucessor s (ou antecessor) do nó deverá ocupar este lugar. Este nó (sucessor) é um descendente que está na subárvore da direita do nó e corresponde ao nó mais à esquerda desta árvore. Ele não tem filhos à esquerda e a sua árvore à direita pode ser movida para o lugar de s. A Figura remove2 ilustra o caso de remoção do nó com o valor 12. Observe que o nó 13 (sucessor) assumiu o lugar do nó 12.

Removendo nó sem filhos

Figura remove0: Removendo nó (8) sem filhos.

Removendo nó com um filho

Figura remov1: Removendo nó (6) com um filho.





Figura remov2: Removendo nó (12) com dois filhos.





 O texto abaixo mosta uma rotina que remove nós de uma árvore, que contém números inteiros. O programa completo está em arvremov.c.

tNo *remover (tNo *tree, int num) {
     tNo *p,  /* p aponta para o no a ser removido */
         *q,  /* q aponta para o pai do no */
         *rp, /* rp aponta que ira substituir o no p */
         *f,
         *s;  /* sucessor do no p */

     p = tree; q=NULL;

     /* procura o no com a chave num, p aponta para o no
     e q aponta para o pai do no */
     while ( p && p->info != num) {
           q = p;
           if ( num < p->info)
              p = p->esq;
           else
               p = p->dir;
     } /* fim do while */
     if (!p) return NULL; /* a chave nao existe na arvore */

     /* agora iremos ver os dois primeiros casos, o no tem um filho
        no maximo */
     if (p->esq == NULL)
        rp = p->dir;
     else
         if (p->dir == NULL)
            rp = p->esq;
         else {
              f=p;
              rp = p->dir;
              s = rp->esq;   /* s e sempre o filho esq de rp */
              while (s != NULL) {
                    f = rp;
                    rp = s;
                    s = rp->esq;
              }
              /* neste ponto, rp e o sucessor em ordem de p */
              if (f != p) {
                 /*  p nao e o pai de rp e rp == f->left */
                 f->esq = rp->dir;
                 /* remove o no rp de sua atual posicao e o
                    substitui pelo filho direito de rp
                    rp ocupa o lugar de p
                 */
                 rp->dir = p->dir;
              }
              /* define o filho esquerdo de rp de modo que rp
                 ocupe o lugar de p
              */
              rp->esq = p->esq;
         }
     /* insere rp na posicao ocupada anteriormente por p */
     if (q == NULL)
        tree = rp;
     else
         if (p == q->esq)
            q->esq = rp;
         else
             q->dir = rp;
     free(p);
     return rp;
}
 
 

------------------------------------------------------------------------Árvores Árvores Binárias Balanceadas

Uma árvore binária balanceada, chamada de árvore AVL, é uma árvore binária na qual as alturas das duas subárvores de cada um dos nós nunca diferem em mais de 1. O balanceamento de um nó é igual a diferença entre as suas altura esquerda e  direita. Portanto, cada nó de uma árvore balanceada tem balanceamento igual a -1, 0 ou 1, dependendo da comparação entre as alturas esquerda e direita. Lembrando que a altura de um nó n da árvore é o número de nós do maior caminho de n até um de seus descendentes. As  folhas tem altura 1. Uma árvore binária completa com n>0 nós tem altura mínima, que é igual a 1 + floor(log (n)). A Figura arvbal mostra uma árvore binária balanceada. Os valores dentro do nó são altura do nó e seu balanceamento.

Árvore binária balanceada
Figura arvbal: Árvore binária balanceada.

Caso a probabilidade de pesquisar uma chave em uma tabela seja a mesma para todas as chaves, uma árvore binária balanceada terá a busca mais eficiente. Infelizmente o método de inserção em árvores binárias apresentado anteriormente não garante que a árvore permanecerá balanceada. Como já vimos a estrutura da árvore depende da ordem em que as chaves são inseridas na árvore.
A Figura arvbali mostra possibilidades de inserção na árvore e o que ocorreria com o seu balanceamento. Cada inserção que mantém a árvore balanceada está indicada bor um B e as que desbalanceiam a árvore por um D. Observe que uma árvore se torna desbalanceada quando o nó inserido se torna descendente esquerdo de um nó que tinha anteriormente um balanceamento de 1 ou se ele se tornar descendente direito de um nó que tinha anteriormente balanceamento de -1. Isto é fácil de deduzir, por exemplo, um nó que tinha balanceamento 1 e recebe um descendente direito aumenta sua altura em 1, portanto aumentando o seu desbalanceamento.


Figura arvbali: Árvore balanceada e suas possibilidades de inserção




Observemos uma subárvore que ser tornará desbalanceada quando ocorrer uma inserção. Vamos supor também que este nó tem um balanceamento de 1. Neste caso o desbalanceamento ocorrerá se a inserção ocorrer em um nó da direita. A Figura arvins mostra um exemplo deste caso.

Figura arvins: Inserção em árvore binária



Observar que o nó A tem balanceamento 1, isto significa que a subárvore da esquerda tem altura não nula e maior em uma unidade que a da direita. Ao inserirmos um nó na subárvore da direita a sua altura aumenta de um e o balanceamento passa para 2. Observar também que como o nó mais jovem a se tornar desbalanceado é o A, o seu filho pela esquerda tem de ter balalneamento igual a 0.

Para manter a árvore balanceada é necessário que a transformação na árvore de tal modo que:
 

  1. a árvore permaneça uma árvore de busca binária;
  2. a árvore continue a ser uma árvore balanceada.
Para isto vamos definir a operação de rotação em uma árvore. Uma rotação pode ser à direita ou esquerda. A Figura arvrot mostra uma árvore e os resultados dos dois tipos de rotação sobre esta árvore estão mostrados na Figura arvrota.

Árvore antes da rotação

Figura arvrot: Árvore original antes da rotação



Efeitos das rotações

Figura arvrota: Efeitos das rotações



Um possível algoritmo para rodar para a esquerda uma árvore enraizada em p é:

q = p->direita;
temp = q->esquerda;
q->esquerda = p;
p->direita = temp;
 

Para verificar o que fazer em uma árvore T após a inserção de um nó q vamos considerar os vários casos possíveis. Primeiro, se após a inclusão todos os nós se mantiveram regulados, então a árvore se manteve AVL e nada há a fazer. Caso contrário vamos considerar o nó p mais próximo das folhas,  que se tornou desregulado. A escolha de p é única, pois qualquer subárvore de T que se tornou desregulada deve incluir p.

Sejam hd(p) e he(p) as alturas direita e esquerda das subárvores de p, portanto

|hd(p) - he(p)| = 2

pois T era AVL.
 

Temos os seguintes casos:

caso 1: hd(p) >he(p)

neste caso q pertence a subárvore esquerda de p. Além disso p possui o filho esquerdo u <> q, senão p não estaria desregulado. Sabe-se também que hd(u) <>he(u), pela mesma razão. Para o caso 1 temos duas possibilidades:

caso 1.1
 

------------------------------------------------------------------------

Exercícios

  1. Escreva um programa que crie uma árvore de busca binária a partir de letras lidas do teclado. O programa deve imprimir a árvore nos três modos de percurso.
    Solução: arv0301.c

  2. Faça uma função que imprima os nós de uma árvore na sequencia de seus níveis.
    Solução: niveis.c

  3. Escreva um programa que leia um arquivo que contem varios numeros inteiros (cada numero em uma linha) e imprima os numeros em ordem crescente (utilize uma arvore para armazenar os numeros na memoria.
    Solução: ordem.c

------------------------------------------

Índice