Arvores - Dei-Isep

Propaganda
ESTRUTURAS DE DADOS NÃO LINEARES
Anteriormente fizemos revisões sobre estruturas lineares (pilha e fila)., isto é, estruturas
que guardam colecções de objectos que são acedidos sequencialmente. Efectivamente
são chamadas lineares porque cada objecto tem um único sucessor.
Em muitas aplicações a organização dos objectos apresenta-se não linear, dado que
qualquer membro pode apresentar múltiplos sucessores. Estudaremos árvores e grafos.
ÁRVORES
Neste tipo de estruturas os elementos não se ligam entre si através de uma relação
anterior-seguinte (caso das estruturas lineares), mas existe uma relação organizacional
mais rica, uma relação hierárquica, são portanto estruturas hierarquizadas. Este tipo de
estrutura representa por exemplo, o organigrama de uma empresa, uma árvore
genealógica, ...um livro, por exemplo, também podemos considerá-lo como estruturado
em árvore, ver a figura abaixo:
Livro
Prefácio
Parte A
Capítulo 1
1.1
1.2
1.3
Capítulo 2
Parte B
Capítulo 3
Capítulo 4
4.1
Capítulo 5
Referências
Capítulo 6
4.2
Definição e Terminologia
A terminologia deste tipo de estrutura é uma terminologia intuitiva que se baseia em
árvores de família, com os termos "pai", "filho", "ascendentes", "descendentes"...
Árvore é assim um tipo abstracto de dados que guarda os elementos (nós)
hierarquicamente.
Na representação gráfica de árvores os nós ligam-se por ramos....
Com excepção do elemento de topo, cada elemento tem um elemento pai e zero ou mais
elementos filhos. O elemento de topo é designado por raiz. Assim, este elemento não
tem elemento pai nem obviamente ascendentes.
Por sua vez os elementos que não possuem filhos são designados por folhas. Os outros
nós da árvore dizem-se interiores.
Por sua vez cada elemento numa árvore é a raiz da subárvore que é definida pelo nó e
todos os descendentes do nó.
Exemplo: descendentes do nó 70 são 60 e 75.
O movimento de um nó para os seus descendentes faz-se através de um único caminho.
Os ascendentes de um nó X são todos os nós que existem no caminho desde esse nó até
à raiz.
Exemplo: ascendentes do nó 85 são 90 e 80.
Define-se ainda por profundidade de um nó o número de ramos existentes no caminho
entre o nó e a raiz, ou recorrendo a uma definição recursiva teremos:
Profundidade do nó (X) =
0 se X é raiz
1 + Profundidade do nó (pai(X)), nos outros casos
Exemplo: Profundidade do nó 85 é 2.
Entende-se por altura de uma árvore a máxima profundidade apresentada pelos nós.
Chamamos grau de um nó ao número de filhos que esse nó tem.
Chamamos grau de uma árvore ao máximo dos graus dos seus nós.
Dizemos que uma árvore é ordenada se existe uma ordem entre os filhos de cada nó,
de tal maneira que podemos identificar os filhos de um nó como sendo o primeiro, o
segundo, ... e essa ordem é importante, não é indiferente, para a definição da nossa
estrutura. Tal ordem é determinada pelo uso que pretendemos fazer da árvore .
ÁRVORES BINÁRIAS
Árvores binárias são árvores ordenadas em que cada nó interno tem no máximo 2 filhos
(fillho esquerdo e fillho direito)
Outra definição possível poderá ser: árvores binárias são árvores que ou são nulas ou
são constituídas por um nó raiz e duas subárvores binárias, a subárvore esquerda e a
subárvore direita (por sua vez cada uma destas subárvores ou são nulas ou constituídas
por um nó raiz e duas subárvores binárias, a a subárvore esquerda e a subárvore
direita,)...
Propriedades
Uma árvore binária com n elementos tem n-1 ramos.
Uma árvore binária de altura h tem no mínimo h elementos e no máximo 2 h+1 -1
Uma árvore binária com 2 h+1 -1 elementos diz-se cheia.
A altura de uma árvore binária com n elementos (n>0) é
no máximo n-1
e
no mínimo log 2 (n+1) -1 , uma vez que: n= 2 h+1 -1
2 h+1 = n + 1
log 2 (2 h+1 ) = log 2 (n + 1)
h = log 2 (n + 1) - 1
Numa árvore binária cheia o número de nós folha é igual ao numero de nós internos + 1.
Métodos de travessia
Temos várias maneiras
forma sistemática..
-
de percorrer ou visitar todos os nós de uma árvore binária de
visita simétrica
visita em preordem
visita em posordem
visita por nível
Visita simétrica
A forma de percorrer os nós da árvore de forma simétrica corresponde a visitar
simètricamente a subárvore esquerda seguida da visita ao nó (por exemplo, escrever
conteúdo do nó ou qualquer outra operação que pretendamos sobre esse nó), seguida da
visita simétrica à subárvore direita.
Assim o algoritmo capaz de executar o que foi dito, poderá ser recursivo e traduzir-se-á
da seguinte forma:
Algoritmo recursivo:
Visita-Simétrica (arvore)
Se (arvore não nula)
Então Visita-Simétrica (subárvore esquerda)
Visita-Nó
Visita-Simétrica (subárvore direita)
Fse
Fim Algoritmo
Se a Visita-Nó for escrever o conteúdo do nó, ao aplicarmos este algoritmo à árvore
acima, obteremos a seguinte sequência de valores :
60 70 75 80 85 90 95
Poder-se-ia usar um algoritmo iterativo, em vez do anteriormente descrito, mas nesse
caso teríamos que usar uma estrutura auxiliar do tipo stack, onde seriam colocados os
apontadores para os nós da árvore que percorremos quando descemos pela subárvore
esquerda . Depois far-se-á pop da stack para visitarmos a subárvore direita.
Algoritmo iterativo
Visita-simétrica
r=raiz
Repete
Enquanto ( r=/= Nulo ) // descer pelos apontadores da esquerda e colocar os nós na stack
Push (r)
r=r->esq
Fenquanto
Se (stack não vazia) // tira o último nó da stack, visita-o e avança para a subárvore direita
Então
r=pop
Visita-Nó
r=r->dir
Fse
Até (stack vazia e r=Nulo)
Fim Algoritmo
Visita em preordem
A forma de percorrer os nós da árvore em preordem, corresponde a visitar o nó,
seguidas das visitas em preordem da subárvore esquerda e da subárvore direita.
Algoritmo recursivo:
Visita-Preordem (arvore)
Se arvore não nula
Então
Visita-Nó
Visita-Preordem (subárvore esquerda)
Visita-Preordem (subárvore direita)
Fse
Fim Algoritmo
Se a Visita-Nó for escrever o conteúdo do nó, ao aplicarmos este algoritmo à árvore
acima, obteremos a seguinte sequência de valores :
80 70 60 75 90 85 95
Do mesmo modo que no caso anterior este tipo de percorrer a árvore binária pode ser
feito utilizando um algoritmo iterativo como abaixo se indica:
Algoritmo iterativo
Visita-Preordem
r=raiz
Repete
Enquanto ( r=/= Nulo ) // descer pelos apontadores da esquerda e colocar os nós na stack
Visita-Nó
Push (r)
r=r->esq
Fenquanto
Se (stack não vazia) // tira o último nó da stack, avança para a subárvore direita
Então
r=pop
r=r->dir
Fse
Até (stack vazia e r=Nulo)
Fim Algoritmo
Visita em posordem
A forma de percorrer os nós da árvore em posordem, corresponde a visitar o nó, no fim,
isto é, depois de ter feito a visita em posordem à subárvore esquerda e a visita em
posordem à subárvore direita.
Algoritmo recursivo:
Visita-Posordem (arvore)
Se (arvore não nula)
Então
Visita-Posordem (subárvore esquerda)
Visita-Posordem (subárvore direita)
Visita-Nó
Fse
Fim Algoritmo
Se a Visita-Nó for escrever o conteúdo do nó, ao aplicarmos este algoritmo à árvore
acima, obteremos a seguinte sequência de valores :
60 75 70 85 95 90 80
Tal como nas visitas anteriores podemos elaborar um algoritmo não recursivo para a
visita em posordem, utilizando também uma stack auxiliar. No entanto, há mais um
pormenor que é preciso atender. Só poderá ser feito o pop definitivo de um elemento da
stack depois de a esse elemento (nó da árvore) ter sido feita a visita em posordem à
subárvore esquerda e à subárvore direita. Será deixado como exercício a elaboração
desse algoritmo sugerindo-se que na stack incluam além do apontador para nó mais um
campo que indica se a esse nó já foram ou não visitadas as duas subárvores.
Visita por níveis
A forma de percorrer os nós da árvore por níveis poderá ser feita utilizando como
estrutura auxiliar uma fila e nesse caso a sequência de valores, da árvore usada nas
visitas anteriores, será a seguinte:
80 70 90 60 75 85 95
Algoritmo
Visita-por-niveis
r=raiz
Se (r não Nula)
Então junta-fila (r)
Fse
Enquanto (fila não vazia)
Retira -fila (r)
Visita-No
Se (r->esq não Nulo)
Então junta-fila (r->esq)
Fse
Se (r->dir não Nula)
Então junta-fila (r->dir)
Fse
Fenquanto
Fim Algoritmo
NOTA 1
Toda a árvore n-ária é possível converter em árvore binária
Para fazer isto basta seguir a seguinte regra: na árvore binária o nó à esquerda é o que na
árvore n-ária era o filho mais à esquerda, se existirem filhos, e o nó à direita é o que na
árvore n-ária era o irmão seguinte (se existir).
Podemos considerar o seguinte algoritmo para executar a referida conversão, desde que
os elementos da árvore n-ária sejam dados em preordem.
Para converter árvore da figura acima aplicando o algoritmo que a seguir se descreve,
os conteúdos dos nós deveriam ser dados na seguinte sequência :
A, B, F, G, C, D, H, I, J, E e ao mesmo tempo indicado o nível em que cada nó se
encontra na árvore original.
Algoritmo
Conversão n-ária-binária
-cria nó raiz
-push (raiz, nível) // usar-se-á uma stack auxiliar
Repete
Ler nó corrente (conteúdo e nível)
Cria nó para árvore binária e atribui-lhe o conteúdo
Se nível do nó corrente > nível do nó da stack
Então
Liga apontador esquerdo do nó da stack (pai) ao nó criado
Senão
Retira da stack todos os nós com nível > que o nível do nó criado
Liga apontador direito do nó da stack ao nó criado
Retira da stack
Fse
Push(nó criado,nível)
Até não haver mais nós.
Fim Algoritmo
NOTA 2
A implementação de árvores binárias pode fazer-se usando alocação dinâmica de
memória, em que será reservado espaço na memória para colocar cada nó. Cada nó será
constituído por um campo informação e dois campos do tipo apontador para nó.
Pode ainda fazer-se a implementação em memória estática utilizando 3 vectores, um
vector com a informação dos nós da árvore e outros dois, vector esquerdo e vector
direito que contêm respectivamente, os índice onde se encontram no vector informação
o nó que está à esquerda e o nó que está à direita.
Implementação dinâmica
Implementação estática
Acima encontram-se representadas graficamente as duas implementações da mesma
árvore.
NOTA 3
Neste tipo de estruturas (arvores nárias e árvores binárias), embora se utilizem em
algumas casos, como é indicado no ponto referente a aplicações, não trazem qualquer
benefício na procura, inserção ou eliminação de um determinado elemento, uma vez que
a construção da estrutura não obedece a nenhum critério que possa facilitar esse tipo de
acções.
Os algoritmos referentes a pesquisa, inserção e eliminação, apresentam uma
complexidade temporal de ordem N.
Aplicações
Este tipo de estrutura tem uma larga aplicação em computação.
Um exemplo dessa utilização são as árvores de decisão, usadas em situações em que se
pretende chegar a uma conclusão dentro de um determinado domínio, através de
respostas do tipo "sim" ou "não", às questões que são formuladas.
A cada nó interno está associada uma pergunta, e se a resposta for "sim" avançaremos
para o filho esquerdo , caso seja "não" para o direito. A cada folha corresponde uma
conclusão.
Outra aplicação possível é a representação de expressões aritméticas em que aos nós
internos associamos operadores e às folhas associamos variáveis ou constantes.
Cada nó tem um valor associado, se for folha é o valor da constante ou da variável, se
for interno é o valor que resulta efectuando a operação, que o operador representa, aos
valores dos seus filhos.
*
+
3
+
6
10
/
45
A árvore acima representa a seguinte expressão :
9
( 3 + 6 ) * ( 10 + 45 / 9 )
Implemantação da estrutura não linear do tipo ARVORE BINARIA em C++
/* Definicao da classe Nodo - representa o nodo da arvore binaria */
template <class T>
class Nodo
{
public:
T * info;
Nodo<T> *esq;
Nodo<T> *dir;
Nodo();
Nodo(const T & e);
Nodo(const T & e,Nodo<T> *esq1,Nodo<T> *dir1);
};
template<class T>
Nodo<T>::Nodo()
{
info=NULL;
esq=dir=NULL;
}
template<class T>
Nodo<T>::Nodo(const T & e)
{
info = new T(e);
esq=dir=NULL;
}
template<class T>
Nodo<T>::Nodo(const T & e,Nodo<T> *esq1,Nodo<T> *dir1)
{
info=new T(e);
esq=esq1;
dir=dir1;
}
/* Fim da classe Nodo */
/* **************************************************** */
/* Definicao da classe arvBinaria - representa a arvore binaria */
template <class T>
class arvBinaria
{
private:
Nodo<T> *raiz;
void destroiArv(Nodo<T> * raiz);
void preOrdem(Nodo<T> *raiz) const;
void postOrdem(Nodo<T> *raiz) const;
void ordemSimetrica(Nodo<T> *raiz) const;
Nodo<T> *eliminar(const T & x,Nodo<T> *rz,int &enc);
Nodo<T> *pesquisar(const T & x,Nodo<T> *rz) const;
public:
arvBinaria();
~arvBinaria();
bool vazia() const;
void preOrdem() const
{
preOrdem(this->raiz); return; }
void postOrdem() const
{
postOrdem(this->raiz); return; }
void ordemSimetrica() const
{
ordemSimetrica(this->raiz); return; }
void fazerArv(const T & elem,arvBinaria<T> & a1,arvBinaria<T> &
a2);
bool pesquisar(const T & x) const;
arvBinaria<T> & eliminar(const T & x);
};
template <class T>
arvBinaria<T>::arvBinaria()
{
raiz = NULL;
}
template <class T>
arvBinaria<T>::~arvBinaria()
{
destroiArv(raiz);
}
template <class T>
void arvBinaria<T>::destroiArv(Nodo<T> * raiz)
{
if (raiz !=NULL)
{
destroiArv(raiz->esq);
destroiArv(raiz->dir);
delete raiz;
}
}
template <class T>
bool arvBinaria<T>::vazia() const
{
return (raiz==NULL);
}
template <class T>
void arvBinaria<T>::preOrdem(Nodo<T> *raiz) const
{
if (raiz!=NULL)
{
cout<< *(raiz->info) << endl;
preOrdem(raiz->esq);
preOrdem(raiz->dir);
}
return;
}
template <class T>
void arvBinaria<T>::postOrdem(Nodo<T> *raiz) const
{
if (raiz!=NULL)
{
postOrdem(raiz->esq);
postOrdem(raiz->dir);
cout<< *(raiz->info) << endl;
}
return;
}
template <class T>
void arvBinaria<T>::ordemSimetrica(Nodo<T> *raiz) const
{
if (raiz!=NULL)
{
ordemSimetrica(raiz->esq);
cout<< *(raiz->info) << endl;
ordemSimetrica(raiz->dir);
}
return;
}
template<class T>
void arvBinaria<T>::fazerArv(const T & elem,arvBinaria<T> &
a1,arvBinaria<T> & a2)
{
raiz = new Nodo<T>(elem,a1.raiz,a2.raiz);
a1.raiz=a2.raiz=NULL;
}
template<class T>
bool arvBinaria<T>:: pesquisar(const T & x) const
{
Nodo<T> *temp;
temp=pesquisar(x,this->raiz); // este é um método privado
if(temp!=NULL)
{
cout <<*(temp->info);
return true;
}
else return false;
}
template<class T>
Nodo<T> * arvBinaria<T>::pesquisar(const T & x,Nodo<T> *rz) const
{
Nodo<T> * n;
if(rz)
{
if(*(rz->info)==x) // a classe com que vai instanciar T terá que ter definido o
//operador ==
return rz;
n=pesquisar(x, rz->esq);
if(n==NULL)
return pesquisar(x, rz->dir);
return n;
}
else return NULL;
}
template<class T>
arvBinaria<T>& arvBinaria<T>::eliminar(const T & x)
{
int enc=0;
raiz=eliminar(x,raiz,enc);
return *this;
}
template<class T>
Nodo<T>* arvBinaria<T>::eliminar(const T & x,Nodo<T> *rz,int &enc)
{
if(rz==NULL)
return rz;
if(*(rz->info)==x)
{
destroiArv(rz);
enc=1;
return NULL;
}
rz->esq=eliminar(x,rz->esq,enc);
if (enc==0)
rz->dir=eliminar(x,rz->dir,enc);
return rz;
}
Download