Escolar Documentos
Profissional Documentos
Cultura Documentos
rvores
W. Celes e J. L. Rangel
Nos captulos anteriores examinamos as estruturas de dados que podem ser chamadas
de unidimensionais ou lineares, como vetores e listas. A importncia dessas estruturas
inegvel, mas elas no so adequadas para representarmos dados que devem ser
dispostos de maneira hierrquica. Por exemplo, os arquivos (documentos) que criamos
num computador so armazenados dentro de uma estrutura hierrquica de diretrios
(pastas). Existe um diretrio base dentro do qual podemos armazenar diversos subdiretrios e arquivos. Por sua vez, dentro dos sub-diretrios, podemos armazenar outros
sub-diretrios e arquivos, e assim por diante, recursivamente. A Figura 13.1 mostra uma
imagem de uma rvore de diretrio no Windows 2000.
Neste captulo, vamos introduzir rvores, que so estruturas de dados adequadas para a
representao de hierarquias. A forma mais natural para definirmos uma estrutura de
rvore usando recursividade. Uma rvore composta por um conjunto de ns. Existe
um n r , denominado raiz, que contm zero ou mais sub-rvores, cujas razes so
ligadas diretamente a r. Esses ns razes das sub-rvores so ditos filhos do n pai, r.
Ns com filhos so comumente chamados de ns internos e ns que no tm filhos so
chamados de folhas, ou ns externos. tradicional desenhar as rvores com a raiz para
cima e folhas para baixo, ao contrrio do que seria de se esperar. A Figura 13.2
exemplifica a estrutura de uma rvore.
n raiz
...
sub-rvores
12-1
+
3
Numa rvore binria, cada n tem zero, um ou dois filhos. De maneira recursiva,
podemos definir uma rvore binria como sendo:
uma rvore vazia; ou
um n raiz tendo duas sub-rvores, identificadas como a sub-rvore da
direita (sad) e a sub-rvore da esquerda (sae).
A Figura 13.4 ilustra a definio de rvore binria. Essa definio recursiva ser usada
na construo de algoritmos, e na verificao (informal) da correo e do desempenho
dos mesmos.
12-2
raiz
vazia
sae
sad
a
b
c
d
Para descrever rvores binrias, podemos usar a seguinte notao textual: a rvore vazia
representada por <>, e rvores no vazias por <raiz sae sad>. Com essa notao,
a
rvore
da
Figura
13.5
representada
por:
<a<b<><d<><>>><c<e<><>><f<><>>>>.
Pela definio, uma sub-rvore de uma rvore binria sempre especificada como
sendo a sae ou a sad de uma rvore maior, e qualquer das duas sub-rvores pode ser
vazia. Assim, as duas rvores da Figura 13.6 so distintas.
a
b
a
b
Isto tambm pode ser visto pelas representaes textuais das duas rvores, que so,
respectivamente: <a <b<><>> <> > e <a <> <b<><>> >.
Estruturas de Dados PUC-Rio
12-3
Da mesma forma que uma lista encadeada representada por um ponteiro para o
primeiro n, a estrutura da rvore como um todo representada por um ponteiro para o
n raiz.
Como acontece com qualquer TAD (tipo abstrato de dados), as operaes que fazem
sentido para uma rvore binria dependem essencialmente da forma de utilizao que se
pretende fazer da rvore. Nesta seo, em vez de discutirmos a interface do tipo abstrato
para depois mostrarmos sua implementao, vamos optar por discutir algumas
operaes mostrando simultaneamente suas implementaes. Ao final da seo
apresentaremos um arquivo que pode representar a interface do tipo. Nas funes que se
seguem, consideraremos que existe o tipo Arv definido por:
typedef struct arv Arv;
12-4
Arv* inicializa(void)
{
return NULL;
}
Para criar rvores no vazias, podemos ter uma operao que cria um n raiz dadas a
informao e suas duas sub-rvores, esquerda e direita. Essa funo tem como valor
de retorno o endereo do n raiz criado e pode ser dada por:
Arv* cria(char c, Arv* sae, Arv* sad){
Arv* p=(Arv*)malloc(sizeof(Arv));
p->info = c;
p->esq = sae;
p->dir = sad;
return p;
}
a1= cria('d',inicializa(),inicializa());
a2= cria('b',inicializa(),a1);
a3= cria('e',inicializa(),inicializa());
a4= cria('f',inicializa(),inicializa());
a5= cria('c',a3,a4);
a = cria('a',a2,a5 );
Alternativamente, a rvore poderia ser criada com uma nica atribuio, seguindo a sua
estrutura, recursivamente:
Arv* a = cria('a',
cria('b',
inicializa(),
cria('d', inicializa(), inicializa())
),
cria('c',
cria('e', inicializa(), inicializa()),
cria('f', inicializa(), inicializa())
)
);
Para tratar a rvore vazia de forma diferente das outras, importante ter uma operao
que diz se uma rvore ou no vazia. Podemos ter:
12-5
int vazia(Arv* a)
{
return a==NULL;
}
Uma outra funo muito til consiste em exibir o contedo da rvore. Essa funo deve
percorrer recursivamente a rvore, visitando todos os ns e imprimindo sua informao.
A implementao dessa funo usa a definio recursiva da rvore. Vimos que uma
rvore binria ou vazia ou composta pela raiz e por duas sub-rvores. Portanto, para
imprimir a informao de todos os ns da rvore, devemos primeiro testar se a rvore
vazia. Se no for, imprimimos a informao associada a raiz e chamamos
(recursivamente) a funo para imprimir os ns das sub-rvores.
void imprime (Arv* a)
{
if (!vazia(a)){
printf("%c ", a->info);
imprime(a->esq);
imprime(a->dir);
}
}
/* mostra raiz */
/* mostra sae */
/* mostra sad */
Exerccio: (a) simule a chamada da funo imprime aplicada arvore ilustrada pela
Figura 13.5 para verificar que o resultado da chamada a impresso de a b d c e f.
(b) Repita a experincia executando um programa que crie e mostre a rvore, usando o
seu compilador de C favorito.
Exerccio: Modifique a implementao de imprime, de forma que a sada impressa
reflita, alm do contedo de cada n, a estrutura da rvore, usando a notao introduzida
anteriormente.
Assim,
a
sada
da
funo
seria:
<a<b<><d<><>>><c<e<><>><f<><>>>>.
Uma outra operao que pode ser acrescentada a operao para liberar a memria
alocada pela estrutura da rvore. Novamente, usaremos uma implementao recursiva.
Um cuidado essencial a ser tomado que as sub-rvores devem ser liberadas antes de se
liberar o n raiz, para que o acesso s sub-rvores no seja perdido antes de sua
remoo. Neste caso, vamos optar por fazer com que a funo tenha como valor de
retorno a rvore atualizada, isto , uma rvore vazia, representada por NULL.
Arv* libera (Arv* a){
if (!vazia(a)){
libera(a->esq);
libera(a->dir);
free(a);
}
return NULL;
}
/* libera sae */
/* libera sad */
/* libera raiz */
Devemos notar que a definio de rvore, por ser recursiva, no faz distino entre
rvores e sub-rvores. Assim, cria pode ser usada para acrescentar (enxertar) uma
sub-rvore em um ramo de uma rvore, e libera pode ser usada para remover
(podar) uma sub-rvore qualquer de uma rvore dada.
Exemplo: Considerando a criao da rvore feita anteriormente:
Estruturas de Dados PUC-Rio
12-6
Arv* a = cria('a',
cria('b',
inicializa(),
cria('d', inicializa(), inicializa())
),
cria('c',
cria('e', inicializa(), inicializa()),
cria('f', inicializa(), inicializa())
)
);
Uma outra funo que podemos considerar percorre a rvore buscando a ocorrncia de
um determinado caractere c em um de seus ns. Essa funo tem como retorno um
valor booleano (um ou zero) indicando a ocorrncia ou no do caractere na rvore.
int busca (Arv* a, char c){
if (vazia(a))
return 0;
/* rvore vazia: no encontrou */
else
return a->info==c || busca(a->esq,c) || busca(a->dir,c);
}
Note que esta forma de programar busca, em C, usando o operador lgico || (ou)
faz com que a busca seja interrompida assim que o elemento encontrado. Isto acontece
porque se c==a->info for verdadeiro, as duas outras expresses no chegam a ser
avaliadas. Analogamente, se o caractere for encontrado na sub-rvore da esquerda, a
busca no prossegue na sub-rvore da direita.
Podemos dizer que a expresso:
return c==a->info || busca(a->esq,c) || busca(a->dir,c);
equivalente a:
12-7
if (c==a->info)
return 1;
else if (busca(a->esq,c))
return 1;
else
return busca(a->dir,c);
inicializa (void);
cria (char c, Arv* e, Arv* d);
vazia (Arv* a);
imprime (Arv* a);
libera (Arv* a);
busca (Arv* a, char c);
/* mostra sae */
/* mostra sad */
/* mostra raiz */
/* libera sae */
/* libera sad */
/* libera raiz */
12-8
Nesse exemplo, podemos notar que o a rvore com raiz no n a tem 3 sub-rvores, ou,
equivalentemente, o n a tem 3 filhos. Os ns b e g tem dois filhos cada um; os ns c e
i tem um filho cada, e os ns d, e, h e j so folhas, e tem zero filhos.
12-9
De forma semelhante ao que foi feito no caso das rvores binrias, podemos representar
essas rvores atravs de notao textual, seguindo o padro: <raiz sa1 sa2 ...
san>. Com esta notao, a rvore da Figura 13.7 seria representada por:
a = <a <b <c <d>> <e>> <f> <g <h> <i <j>>>>
<d>>
<b <c <d>> <e>>
<j>>
<g <h> <i <j>>>
= <a <b <c <d>> <e>> <f> <g <h> <i <j>>>>
Representao em C
Dependendo da aplicao, podemos usar vrias estruturas para representar rvores,
levando em considerao o nmero de filhos que cada n pode apresentar. Se
soubermos, por exemplo, que numa aplicao o nmero mximo de filhos que um n
pode apresentar 3, podemos montar uma estrutura com 3 campos para apontadores
para os ns filhos, digamos, f1 , f2 e f3 . Os campos no utilizados podem ser
preenchidos com o valor nulo NULL, sendo sempre utilizados os campos em ordem.
Assim, se o n n tem 2 filhos, os campos f1 e f2 seriam utilizados, nessa ordem, para
apontar para eles, ficando f3 vazio. Prevendo um nmero mximo de filhos igual a 3, e
considerando a implementao de rvores para armazenar valores de caracteres simples,
a declarao do tipo que representa o n da rvore poderia ser:
struct arv3 {
char val;
struct no *f1, *f2, *f3;
};
A Figura 13.8 indica a representao da rvore da Figura 13.7 com esta organizao.
Como se pode ver no exemplo, em cada um dos ns que tem menos de trs filhos, o
espao correspondente aos filhos inexistentes desperdiado. Alm disso, se no existe
um limite superior no nmero de filhos, esta tcnica pode no ser aplicvel. O mesmo
acontece se existe um limite no nmero de ns, mas esse limite ser raramente
alcanado, pois estaramos tendo um grande desperdcio de espao de memria com os
campos no utilizados.
12-10
Uma soluo que leva a um aproveitamento melhor do espao utiliza uma lista de
filhos: um n aponta apenas para seu primeiro (prim) filho, e cada um de seus filhos,
exceto o ltimo, aponta para o prximo (prox) irmo. A declarao de um n pode ser:
struct arvgen {
char info;
struct arvgen *prim;
struct arvgen *prox;
};
A Figura 13.9 mostra o mesmo exemplo representado de acordo com esta estrutura.
Uma das vantagens dessa representao que podemos percorrer os filhos de um n de
forma sistemtica, de maneira anloga ao que fizemos para percorrer os ns de uma lista
simples.
12-11
12-12
(char c)
=(ArvGen *) malloc(sizeof(ArvGen));
c;
NULL;
NULL;
A funo que insere uma nova sub-rvore como filha de um dado n muito simples.
Como no vamos atribuir nenhum significado especial para a posio de um n filho, a
operao de insero pode inserir a sub-rvore em qualquer posio. Neste caso, vamos
optar por inserir sempre no incio da lista que, como j vimos, a maneira mais simples
de inserir um novo elemento numa lista encadeada.
void insere (ArvGen* a, ArvGen* sa)
{
sa->prox = a->prim;
a->prim = sa;
}
Com essas duas funes, podemos construir a rvore do exemplo da Figura 13.7 com o
seguinte fragmento de cdigo:
/* cria ns como folhas */
ArvGen* a = cria('a');
ArvGen* b = cria('b');
ArvGen* c = cria('c');
ArvGen* d = cria('d');
ArvGen* e = cria('e');
ArvGen* f = cria('f');
ArvGen* g = cria('g');
ArvGen* h = cria('h');
ArvGen* i = cria('i');
ArvGen* j = cria('j');
/* monta a hierarquia */
insere(c,d);
insere(b,e);
insere(b,c);
insere(i,j);
insere(g,i);
insere(g,h);
insere(a,g);
insere(a,f);
insere(a,b);
Para imprimir as informaes associadas aos ns da rvore, temos duas opes para
percorrer a rvore: pr-ordem, primeiro a raiz e depois as sub-rvores, ou ps-ordem,
primeiro as sub-rvores e depois a raiz. Note que neste caso no faz sentido a ordem
simtrica, uma vez que o nmero de sub-rvores varivel. Para essa funo, vamos
optar por imprimir o contedo dos ns em pr-ordem:
void imprime (ArvGen* a)
{
ArvGen* p;
printf("%c\n",a->info);
for (p=a->prim; p!=NULL; p=p->prox)
imprime(p);
}
Estruturas de Dados PUC-Rio
12-13
A ltima operao apresentada a que libera a memria alocada pela rvore. O nico
cuidado que precisamos tomar na programao dessa funo a de liberar as subrvores antes de liberar o espao associado a um n (isto , usar ps-ordem).
void libera (ArvGen* a)
{
ArvGen* p = a->prim;
while (p!=NULL) {
ArvGen* t = p->prox;
libera(p);
p = t;
}
free(a);
}
12-14