Você está na página 1de 38

Projeto de Algoritmos

Home | Prefácio | Livros | Sítios WWW | Índice

As árvores da computação têm a tendência de crescer para baixo:


a raiz fica no ar enquanto as folhas se enterram no chão.
— folclore

Árvores binárias
(Veja o verbete Binary tree na Wikipedia.)

Uma árvore binária é uma estrutura de dados mais geral que uma lista encadeada. Este capítulo
introduz as operações mais simples sobre árvores binárias. O capítulo seguinte trata de uma aplicação
básica.

Nós e filhos
Uma árvore binária (= binary tree) é um conjunto de registros que satisfaz certas condições. (As
condições não serão dadas explicitamente, mas elas ficarão implicitamente claras no contexto.) Os
registros serão chamados nós (poderiam também ser chamados células). Cada nó tem um endereço.
Suporemos por enquanto que cada nó tem três campos: um número inteiro e dois ponteiros para nós.
Os nós podem, então, ser definidos assim:

conteudo
struct cel {
999
int conteudo; /* conteúdo */
esq dir
struct cel *esq;

struct cel *dir;

};

typedef struct cel no; /* nó */

O campo conteudo é a "carga útil" do nó; os outros dois campos servem apenas para dar estrutura à
árvore. O campo esq de todo nó contém o endereço de outro nó ou NULL. A mesma hipótese vale
para o campo dir. Se o campo esq de um nó X é o endereço de um nó Y, diremos que Y é o filho
esquerdo de X. Analogamente, se X.dir é igual a &Y então Y é o filho direito de X.
Se um nó Y é filho (esquerdo ou direito) de X, diremos que X é o pai de Y. Uma folha (= leaf) é um
nó que não tem filho algum.

É muito conveniente confundir cada nó com seu endereço. Assim, se x é um ponteiro para um nó (ou
seja, se x é do tipo *no), dizemos simplesmente "considere o nó x" em lugar de "considere o nó cujo
endereço é x".

Árvores e subárvores
Suponha que x é (o endereço de) um nó. Um descendente de x é qualquer nó que possa ser
alcançada pela iteração dos comandos x = x->esq e x = x->dir em qualquer ordem. (É claro que
esses comandos só podem ser iterados enquanto x for diferente de NULL. Estamos supondo que NULL é
de fato atingido mais cedo ou mais tarde.)

Um nó x juntamente com todos os seus descendentes é uma árvore binária. Dizemos que x é a raiz
(= root) da árvore. Se x tiver um pai, essa árvore é uma subárvore de alguma árvore maior. Se x é
NULL, a árvore é vazia.

Para qualquer nó x, x->esq é a raiz da subárvore esquerda de x e x->dir é a raiz da subárvore


direita de x.

Endereço de uma árvore e definição recursiva


O endereço de uma árvore binária é o endereço de sua raiz. É conveninente confundir árvores com
seus endereços: dizemos "considere a árvore r" em lugar de "considere a árvore cuja raiz tem
endereço r".

Essa convenção sugere a introdução do nome alternativo arvore para o tipo-de-dados ponteiro-para-
nó:

typedef no *arvore; /* árvore */

A convenção permite formular a definição deárvore binária de maneira recursiva: um objeto r é uma
árvore binária se

1. r é NULL ou
2. r->esq e r->dir são árvores binárias.

Muitos algoritmos sobre árvores ficam mais simples quando escritos de forma recursiva.

Exercício
1. Árvores binárias têm uma relação muito íntima com certas seqüências bem-formadas de
parênteses. Discuta essa relação.
2. Árvores binárias podem ser usadas, de maneira muito natural, para representar expressões
aritméticas (como ((a+b)*c-d)/(e-f)+g, por exemplo). Discuta os detalhes.

Varredura esquerda-raiz-direita
Ao contrário de uma lista encadeada, uma árvore binária pode ser percorrida de muitas maneiras
diferentes. Uma maneira particularmente importante é a ordem esquerda-raiz-direita. Na varredura
e-r-d (= inorder traversal), visitamos

1. a subárvore esquerda da raiz, em ordem e-r-d;


2. depois a raiz;
3. depois a subárvore direita da raiz, em ordem e-r-d.

Na figura abaixo, os nós estão numeradas na ordem da varredura e-r-d.

5
/ \

3 8
/ \ / \
1 4 6 9
/ \ \
0 2 7

Eis uma função recursiva que faz a varredura e-r-d de uma árvore binária r:

// Recebe a raiz r de uma árvore binária.

// Imprime os conteúdos dos nós em ordem e-r-d.

void erd (arvore r) {

if (r != NULL) {

erd (r->esq);

printf ("%d\n", r->conteudo);

erd (r->dir);

}
}

É um excelente exercício escrever uma versão iterativa desta função. Nossa versão usa uma pilha
p[0..t-1] de endereços e mais um endereço x que é candidato a entrar na pilha; é como se a pilha
fosse

p[0], p[1], . . . , p[t-1], x .

A seqüência x, p[t-1], . . . , p[0] é uma espécie de "roteiro" daquilo que ainda precisa ser feito: x
representa a instrução "imprima a árvore x" e cada p[i] representa a instrução "imprima o nó p[i] e
em seguida a árvore p[i]->dir". Para dimensionar a pilha, suporemos que nossa árvore não tem
mais que 100 nós.

// Recebe a raiz r de uma árvore binária.

// Imprime os conteúdos dos nós em ordem e-r-d.

// Supõe que a árvore não tem mais que 100 nós.

void erd_i (arvore r) {

no *x, *p[100];

int t = 0;

x = r;

while (x != NULL || t > 0) {

// a pilha é p[0..t-1]; o índice do topo é t-1

if (x != NULL) {

p[t++] = x;

x = x->esq;

else {

x = p[--t];

printf ("%d\n", x->conteudo);

x = x->dir;

}
}

Para a árvore sugerida na figura acima, a pilha p evolui como indicado na tabela abaixo. Cada linha
da tabela resume o estado de coisas no início de uma iteração: à esquerda estão os nós que já foram
impressos; à direita está a pilha x, p[t-1], . . . , p[0]. O valor NULL está indicado por N.

3 5

1 3 5

0 1 3 5

N 0 1 3 5

0 N 1 3 5

0 1 2 3 5

0 1 N 2 3 5

0 1 2 N 3 5

0 1 2 3 4 5

0 1 2 3 N 4 5

0 1 2 3 4 N 5

0 1 2 3 4 5 8

0 1 2 3 4 5 6 8

0 1 2 3 4 5 N 6 8

0 1 2 3 4 5 6 7 8

0 1 2 3 4 5 6 N 7 8

0 1 2 3 4 5 6 7 N 8

0 1 2 3 4 5 6 7 8 9

0 1 2 3 4 5 6 7 8 N 9

0 1 2 3 4 5 6 7 8 9 N

Exercícios
3. Verifique que o código abaixo é equivalente ao da função erd:
4. while (1) {
5. while (x != NULL) {
6. p[t++] = x;
7. x = x->esq;
8. }
9. if (t == 0) break;
10. x = p[--t];
11. printf ("%d\n", x->conteudo);
12. x = x->dir;
13. }

14. Escreva uma função que calcule o número de nós de uma árvore binária.

15. Escreva uma função que imprima, em ordem e-r-d, os conteúdos das folhas de uma árvore
binária.

16. Dada uma árvore binária, encontrar um nó da árvore cujo conteúdo tenha um certo valor k.

17. Escreva uma função que faça varredura r-e-d (= preorder traversal) de uma árvore binária.
[A varredura r-e-d também é conhecida como "busca em profundidade" ou depth-first search.]

18. Escreva uma função que faça varredura e-d-r (= postorder traversal) de uma árvore binária.

19. Discuta a relação entre a varredura e-r-d e a notação infixa de expressões aritméticas. Discuta
a relação entre a varredura e-d-r e a notação posfixa. (Veja exercício acima. Veja também a
seção sobre notação polonesa.)

Primeiro e último nós


Considere o seguinte problema: encontrar o endereço do primeiro nó na ordem e-r-d. É claro que o
problema só faz sentido se a árvore não é vazia, ou seja, se r é diferente de NULL. Eis uma função que
resolve o problema:

// Recebe uma árvore binária não-vazia r.

// Devolve o primeiro nó da árvore na ordem e-r-d.

no *primeiro (arvore r) {

while (r->esq != NULL)

r = r->esq;

return r;

}
Não é difícil fazer uma função análoga que encontre o último nó na ordem e-r-d.

Exercícios
9. Escreva uma versão recursiva da função primeiro.

10. Escreva uma função que encontre o último nó na ordem e-r-d.

Altura
A altura de um nó x em uma árvore binária é a distância entre x e o seu descendente mais afastado.
Mas precisamente, a altura de x é o número de passos do mais longo caminho que leva de x até uma
folha. Os caminhos a que essa definição se refere são os obtido pela iteração dos comandos x = x-
>esq e x = x->dir, em qualquer ordem.

A altura de uma árvore é a altura da raiz da árvore. Uma árvore com um único nó tem altura 0. A
árvore da figura tem altura 3.

/ \

D I
/ / \

B G K
/ \ / \ /
A C F H J

Eis como a altura de uma árvore com raiz r pode ser calculada:

// Devolve a altura da árvore binária cuja raiz é r.

int altura (arvore r) {

if (r == NULL)

return -1; // altura de árvore vazia é -1

else {

int he = altura (r->esq);


int hd = altura (r->dir);

if (he < hd) return hd + 1;

else return he + 1;

Qual a relação entre a altura, digamos h, e o número de nós, digamos n, de uma árvore binária?
Resposta:

n-1 ≥ h ≥ lg(n) ,

onde lg(n) denota o piso de log2 n .

n lg(n)
4 2
5 2
6 2
10 3
64 6
100 6
128 7
1000 9
1024 10
1000000 19

Uma árvore binária com h = n-1 é um "tronco sem galhos": cada nó tem no máximo um filho. No
outro extremo, uma árvore com h = lg(n) é "quase completa": todos os "níveis" estão lotados exceto
talvez o último.

/ \

D K
/ \ / \

B F J L
/ \ / \ /
A C E G I
Uma árvore binária é balanceada (ou equilibrada) se, em cada um de seus nós, as subárvores
esquerda e direita tiverem aproximadamente a mesma altura. Uma árvore binária balanceada com n
nós tem altura próxima de lg(n).

Convém trabalhar com árvores balanceadas sempre que possível. Mas isso não é fácil se a árvore
aumenta e diminui ao longo da execução do seu programa.

Exercícios
11. Desenhe uma árvore binária que tenha conteúdos 1, . . . , 17 e a menor altura possível. Repita
com a maior altura possível.

12. Escreva uma função iterativa para calcular a altura de uma árvore binária.

13. Uma árvore é balanceada no sentido AVL se, para cada nó x, as alturas das subárvores que
têm raízes x->esq e x->dir diferem de no máximo uma unidade. Escreva uma função que
decida se uma dada árvore é balanceada no sentido AVL. Procure escrever sua função de
modo que ela visite cada nó no máximo uma vez.

Nós com campo pai


Em algumas aplicações (veja seção seguinte) é conveniente ter acesso imediato ao pai de qualquer nó.
Para isso, é preciso acrescentar um campo pai a cada nó:

esq pai esq


struct cel {

int conteudo; 999

struct cel *pai; esq dir

struct cel *esq;

struct cel *dir;

};

typedef struct cel no;

É um bom exercício escrever uma função que preencha o campo pai de todos os nós de uma árvore
binária.
Exercícios
14. Escreva uma função que preencha corretamente todos os campos pai de uma árvore binária.

15. A profundidade (= depth) de um nó x em uma árvore binária com raiz r é a distância entre x e
r. Mais precisamente, a profundidade de x é o comprimento do (único) caminho que vai de r
até x. Por exemplo, a profundidade de r é 0 e a profundidade de r->esq é 1. Escreva uma
função que determine a profundidade de um nó em relação à raiz da árvore.

16. Escreva uma função que imprima os conteúdos de uma árvore binária com recuos de margem
proporcionais à profundidade do nó na árvore. Por exemplo, a árvore

555
/ \

333 888
/ \ \
111 444 999

deve se representada assim:

555

333

111

444

888

999

onde os caracteres '-' representam NULL.


17. Em que condições uma árvore binária é um max-heap? Escreva uma função que transforme
uma árvore binária quase completa em heap.

Nó seguinte e anterior (sucessor e predecessor)


Digamos que x é o endereço de um certo nó de uma árvore binária. Nosso problema: calcular o
endereço do nó seguinte na ordem e-r-d.

Para resolver o problema, é necessário que os níos tenham um campo pai. Eis uma função que
resolve o problema. É claro que a função só deve ser chamada com x diferente de NULL. A função
devolve o endereço do nó seguinte a x ou devolve NULL se x é o último nó. (Note que a função não
precisa saber onde está a raiz da árvore.)

// Recebe o endereço de um nó x. Devolve o endereço

// do nó seguinte na ordem e-r-d.

// A função supõe que x != NULL.

no *seguinte (no *x) {

if (x->dir != NULL) {

no *y = x->dir;

while (y->esq != NULL) y = y->esq;

return y; // *

while (x->pai != NULL && x->pai->dir == x) // **

x = x->pai; // **

return x->pai;

}
Comentários: Na linha *, y é o endereço do primeiro nó, na ordem e-r-d, da subárvore que tem raiz x-
>dir. As linhas ** fazem com que x suba na árvore enquanto for filho direito de alguém.

Exercícios
18. Escreva uma função que receba o endereço de um nó x de uma árvore binária e encontre o
endereço do nó anterior a x na ordem e-r-d.

19. Escreva uma função que faça varredura e-r-d usando as funções primeiro e seguinte.

URL of this site: www.ime.usp.br/~pf/algoritmos/


1998 | Last modified: Fri Oct 2 07:28:10 BRT 2009
Paulo Feofiloff
IME-USP
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.
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.

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:

• nível 0 = A
• nível 1 = B, C
• nível 2 = D, E, F, G
• nível 3 = H, I

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.
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.

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.

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.
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.

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.

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.
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);
}
}

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.
Figura arvhuf1: Árvore de Huffman

Vamos considerar outro exemplo em que temos


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.
Figura remove0: Removendo nó (8) sem filhos.

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.
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.
Figura arvrot: Árvore original antes da rotação

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

Você também pode gostar