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