Árvores Seminário de Matemática Discreta Componentes: Rogério Caetano Cardoso, Nino Prates Arcanjo, Leonardo Duarte Vencioneck, Vinicius Maia. Professor: Leandro Colombi Resendo Curso: Bacharelado em Sistema de Informação - IFES Campus Serra ÍNDICE Árvores (Definição e terminologias) 2 Árvores Binárias 4 Armazenamento de Árvores Binárias 5 Aplicação 6 Como percorrê-la 6 Algoritmos 6 Exercícios 13 Referências 16 Definição: Uma árvore T é um conjunto finito de elementos denominados nós ou vértices tais que: T = 0 é a árvore dita vazia ou existe um nó r, chamado raiz de T; os nós restantes constituem um único conjunto vazio ou são divididos em m<1 conjuntos distintos não vazios que são as sub-árvores de r, cada sub-árvore a qual é, por sua vez, uma árvore. Exemplo: Terminologia: Sub-árvore: Cada nó de árvore é a raiz de uma sub-árvore. Grau de um nó: O número de sub-árvores de um nó é o grau daquele nó. Ex: O nó F tem grau 2. Grau de uma árvore: O grau de uma árvore é o máximo entre os graus de seus nós. Ex: A árvore acima tem grau 2. Filhos de um nó X: são as raízes das sub-árvores de um nó. Ex: O nó D é filho do nó B. Folha: Um nó de grau igual a zero é denominado folha ou nó terminal. Ex: As folhas dessa árvore são os nós: D, E, H, I, G. Nível: A raiz da árvore tem sempre nível 0 e o nível dos demais nós é igual ao número de linhas que o liga a raiz, isto é o comprimento do caminho que vai da raiz até este nó. Ex: O nível do nó H é 3. Altura: É definida pelo nível mais alto da Árvore. Ex: A tem nível 0; B e C nível 1; D, E, F, G tem nível 2; H e I tem nível 3 e esse é o nivel mais alto, portanto, a altura da árvore é 3. Dica: Sempre será uma folha que terá o maior nível da árvore. Terminologia: Floresta: É um conjunto de zero ou mais árvores disjuntas. EX: Árvore Ordenada: É aquela na qual os filhos de cada nó estão ordenados. Assume – se a ordenação da esquerda para direita. Ex: Ordenada Não Ordenada Árvores Isomorfas: Duas árvores não ordenadas são isomorfas quando puderem se tornar coincidentes através de uma permutação na ordem das sub-árvores de seus nós. Duas árvores ordenadas são isomorfas quando forem coincidentes segundo a ordenação existente entre seus nós. Terminologia: Árvore Cheia: Árvore com número máximo de nós. Uma árvore de grau x tem número máximo de nós se cada nó, com exceção das folhas tem grau x. Ex: Árvore cheia de grau 2: Árvore binária: Árvore binária é aquela em que cada nó tem no máximo duas sub-árvores (filhos), onde é necessária a distinção entre a da esquerda e a da direita. Árvores binárias são utilizadas em várias situações, por exemplo, um torneio de futebol eliminatório, do tipo das copas do mundo, 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. 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. Observe a figura abaixo e veja que são duas árvores idênticas, mas são duas árvores binárias diferentes, pois uma tem uma simples inversão nas folhas da árvore gera duas árvores binárias diferentes. Existem alguns tipos de árvores binárias: Árvore estritamente binária: é aquela em que cada nó tem 0 ou 2 filhos. Árvore binária cheia: é a que se um nó tem alguma sub-árvore vazia então ele está no último nível. Árvore binária completa: é a que se n é um nó com algumas de sub-arvores vazias, então n se está no penúltimo ou no último nível. Logo, toda árvore cheia é completa e estritamente binária. Armazenamento de Árvores Binárias (Programação): 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. Claro que também devemos ter o campo para o armazenamento das informações contidas em cada nó. Para armazenar uma árvore com n nós precisamos de 2n+1 unidades de memória. Exemplo de Aplicação de Árvores Binárias: Árvores binárias são estruturas importantes toda vez que uma decisão binária deve ser tomada em algum ponto de um algoritmo. Vamos supor que precisamos descobrir os números que estão duplicados em uma lista não ordenada de números. Existem muitas maneiras de se realizar essa tarefa, Uma delas seria comparar cada novo número com todos os números já lidos, o que aumenta muito a complexidade do algoritmo. Outra maneira é usar uma árvore binária para manter os números, onde o primeiro número lido é colocado na raiz da árvore e cada novo número lido é comparado com o elemento raiz, caso seja igual é uma duplicata e voltamos a ler outro número. Se for menor repetimos o processo com a árvore da esquerda e se maior com a árvore da direita. 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. Percorrendo Árvores Binárias Percorrer uma árvore binária é passar por todos os nós, pelo menos uma vez. Visitar um nó quer dizer que será executada uma operação com a informação que está armazenada nesse nó, por exemplo, imprimir seu conteúdo. Quando se percorre uma árvore binária pode-se passar por alguns nós mais de uma vez, sem porém visitálos.Não existe uma ordem natural para percorrer árvores, logo existem diferentes maneiras de percorrê-las. Um método é o percurso em pré-ordem, implica em executar recursivamente os três passos nessa 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 abaixo este percurso forneceria, no caso da visita significar imprimir, os seguintes resultados: F B A D C E H G I. Algoritmo de Exibição: void exibirEmOrdem(No *pRaiz){ if(pRaiz != NULL){ exibirEmOrdem(pRaiz->pEsquerda); printf("\n%i", pRaiz->numero); exibirEmOrdem(pRaiz->pDireita); } } Esse algoritmo, é um algoritmo recursivo em C, que imprime na tela um nó, com suas ramificações inferiores. Imprimindo primeiro, todo o ramo da esquerda, pra depois imprimir as ramificações da direita. Algoritmo para se contar nós: int contarNos(No *pRaiz){ if(pRaiz == NULL) return 0; else return 1 + contarNos(pRaiz->pEsquerda) + contarNos(pRaiz->pDireita); } Neste algoritmo, ele percorre todos os nós, ou elementos, da árvore, e enquanto ele existir, ele conta soma esse elemento, com suas ramificações. Algoritmo para se contar folhas: int contarFolhas(No *pRaiz){ if(pRaiz == NULL) return 0; if(pRaiz->pEsquerda == NULL && pRaiz->pDireita == NULL) return 1; return 0 + contarFolhas(pRaiz->pEsquerda) + contarFolhas(pRaiz->pDireita); } Já este algoritmo, soma apenas quando não há ramificações adjacentes ao elemento passado a ela como parâmetro, no caso, apenas para contar as folhas. Algoritmo de Inserção Para inserirmos valores em uma árvore binária nós utilizamos um procedimento recursivo que recebe como parâmetro o um nó da arvore e o elemento que vai ser inserido na árvore. Com isso a árvore é percorrida até que encontremos a posição no qual o elemento deve ser armazenado e inserimos o mesmo na árvore. Logo abaixo temos um pseudocódigo com uma estrutura que representa uma arvore binária e o algoritmo de inserção na mesma. estrutura No inicio inteiro valor; No dir, esq; fim vazio arvore_inserir(No raiz, inteiro valor) inicio se(raiz=NULO) inicio raiz.valor<-valor; raiz.esq<-NULO; raiz.dir<-NULO; fim se senao inicio se(v>raiz.valor) inicio arvore_inserir(raiz.dir,valor); fim se senao inicio arvore_inserir(raiz.esq,valor); fim senao fim senao fim Algoritmo de Remoção Para removermos um nó de uma árvore devemos levar analisar três casos: o nó não possui filhos; o nó possui apenas um filho; o nó possui dois filhos. No primeiro caso nós iremos simplesmente armazenar o valor NULO no nó que será removido. Removendo nó (8) sem filhos No segundo o nó filho irá “subir”, ou seja, nós iremos mover o nó filho daquele que será removido uma posição acima. Removendo nó (6) com um filho O terceiro caso é o mais complexo. Aqui nós iremos percorrer a subárvore direita do nó (ou o filho direito do nó) procurando o elemento mais à esquerda dessa subárvore, ou seja, o menor elemento. Ao encontrarmos esse elemento, caso ele tenha um filho, esse filho estará à direita, então esse filho à direita será movido para a posição acima e o pai será movido para a posição do nó que será removido. Caso o elemento mais à esquerda não tenha filho então nós simplesmente movemos ele para o nó que será removido e armazenamos o valor NULO na posição na qual ele se encontrava. Removendo nó (12) com dois filhos Um pseudo-código do algoritmo de remoção é mostrado abaixo. Nele nós usamos uma função auxiliar com o nome percorrer para o caso de um nó com dois filhos. Essa função percorre a subárvore direita do nó que vai ser removido buscando o elementos mais à esquerda da mesma, retorna esse elemento e armazena o valor NULO na posição anterior desse elemento, caso ele não tenha filho ou, caso ele tenha filho, move o filho uma posição acima. Para esse algoritmo considere a mesma estrutura que foi usada no algoritmo de inserção para representar a árvore. vazio arvore_remover(No raiz, inteiro valor) inicio inteiro aux; se(raiz.valor=valor) inicio se(raiz.dir=NULO e raiz.esq=NULO) inicio raiz<-NULO; fim se senao inicio se(raiz.esq!=NULO e raiz.dir!=NULO) inicio aux=percorrer(raiz.dir); raiz.valor<-aux; fim senao se(raiz.esq!=NULO) inicio raiz.valor<-raiz.esq.valor; raiz.esq<-NULO; fim se senao inicio raiz.valor<-raiz.dir.valor; raiz.dir<-NULO; fim senao fim senao fim senao fim se senao inicio se(valor>raiz.valor) inicio arvore_remover(raiz.dir,valor); fim se senao inicio arvore_remover(raiz.esq,valor); fim senao fim senao fim inteiro percorrer(No raiz) Inicio inteiro aux; se(raiz.esq=NULO) inicio se(raiz.dir=NULO) inicio aux<-raiz.valor; raiz<-NULO; retorna aux; fim se senao inicio aux<-raiz.valor; raiz.valor<-raiz.dir.valor; raiz.dir.valor<-NULO; retorna aux; fim senao fim se senao inicio retorna percorrer(raiz.esq); fim senao fim Exercícios: 1) Desenhe a árvore que represente a expressão abaixo: (GERSTING, J. L., Fundamentos matemáticos para a ciência da computação, Ed. LTC, 5ª Ed. 2008, Capítulo 5.2 – exercício 3 ). [( x – 2 ) * 3 ] + ( 5 + 4 ) Resolução: 2) Monte a árvore binária de acordo com a tabela abaixo: (GERSTING, J. L., Fundamentos matemáticos para a ciência da computação, Ed. LTC, 5ª Ed. 2008, Capítulo 5.2 – exercício 7 [modificado]) Resolução: 3) Dada a árvore abaixo, indique qual elemento é a raiz, os nós e as folhas: Resolução: Raiz: A Nós: B, D, G Folhas: E, F, C, H, I Referências: http://equipe.nce.ufrj.br/adriano/c/apostila/arvore.htm Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest e Clifford Stein. Algoritmos: teoria e prática. 2002. GERSTING, J. L., Fundamentos matemáticos para a ciência da computação, Ed. LTC, 5ª Ed. 2008, Capítulo 5.2