Arvores Uma das mais importantes classes de estruturas de dados em computação são as árvores. Aproveitando-se de sua organização hierárquica, muitas aplicações são realizadas usando-se algoritmos relativamente simples, recursivos e de eficiência bastante razoável. Uma árvore é uma estrutura de dados que se caracteriza por uma relação de hierarquia entre os elementos que a compõem.Como exemplos de estruturas em árvores: • O organograma de uma empresa; • A divisão de um livro em capítulos, seções, tópicos, etc; • A árvore genealógica de uma pessoa. Uma árvore consiste de nós conectados por bordas (ou galhos). Os nós são geralmente representados por círculos e os galhos por linhas conectando os círculos. As árvores têm sido estudadas extensivamente e são utilizadas em várias áreas de conhecimento. Elas são na verdade uma instância de uma categoria mais geral, os grafos. Um galho por sua vez, é geralmente implementado como um ponteiro dentro de um nó que aponta para outro nó. Normalmente há um nó na fila de cima de uma árvore, com os galhos conectando mais nós na segunda fila, ainda mais na terceira, etc. Desta forma, as árvores são pequenas em cima e grandes embaixo. Ela pode parecer de cabeça para baixo se comparada a uma árvore de verdade, mas é a forma mais natural de um programa trabalhar, indo da ponta menor para baixo. Existem diferentes tipos de árvores. Algumas possuem mais de dois filhos por nó (veremos adiante o que são filhos). Estudaremos aqui um tipo especializado de árvores, chamado de árvore binária, onde cada nó possui, no máximo, dois filhos. A B D C E H G F I J Árvo re Terminologia Percurso: É a seqüência obtida quando se anda de nó em nó pelos galhos que os conectam. Percorrer a árvore: Percorrer uma árvore significa visitar todos os nós em alguma ordem especificada, por exemplo, na ordem ascendente dos valores-chave. Raiz: É o nó em cima da árvore. Há somente uma raiz em uma árvore. Para que um conjunto de nós e galhos possa ser definido como uma árvore, deve haver apenas um percurso da raiz para qualquer outro nó. Pai: Qualquer nó (exceto a raiz), possui exatamente um galho indo para cima até outro nó. Este nó acima é chamado de nó pai. Filho: Qualquer nó pode ter um ou mais galhos indo para baixo até outros nós. Estes nós abaixo de um dado nó são chamados de seus filhos. Folha: Um nó que não possui filhos é chamado de folha. Só pode haver uma raiz em uma árvore, mas pode haver muitas folhas. Sub-árvore: Qualquer nó pode ser considerado a raiz de uma sub-árvore, que consiste de seus filhos, e os filhos destes, etc. Pensando em termos de família, a sub-árvore de um nó contém todos os seus descendentes. Visitar um nó: Um nó é visitado quando o controle de um programa chega no nó, geralmente para realizar alguma operação neste, tal como verificar o valor de seus dados, modificá-los ou exibi-los. Apenas passar sobre um nó no percurso de um a outro é considerado visitar o nó. Nível: O nível de um nó em particular se refere a quantas gerações o nó está da raiz. Se assumirmos que a raiz está no nível 0, os seus filhos estarão no nível 1, seus netos no nível 2, etc. Chaves: É o valor utilizado para procurar pelo item ou realizar operações sobre ele. Geralmente aparece nos diagramas de árvores, dentro do círculo que representa um nó. Árvores binárias: Se cada nó em uma árvore pode ter no máximo dois filhos, a árvore é chamada de árvore binária. Elas são as mais simples, mais comuns, e em muitas situações as árvores mais utilizadas. Filho esquerdo e direito: Os filhos de cada nó em uma árvore binária são chamados de filho esquerdo e filho direito. Um nó em uma árvore binária não necessariamente possui dois filhos; pode ter apenas um filho esquerdo, ou apenas um filho direito, ou pode não ter filhos (o que significa que é uma folha). A maioria das operações em árvores envolve descer até o nível mais baixo da árvore. Em uma árvore completa, metade dos nós está no nível inferior. Desta forma, cerca de metade de todas as procuras, inserções ou exclusões requerem encontrar um nó no nível mais baixo. Durante uma procura, visitamos um nó de cada nível. Percorrer uma árvore não é tão rápido, mas esta não é uma operação muito freqüente de se realizar num banco de dados grande comum. Um percurso é necessário na análise de expressões algébricas, mas neste caso a árvore provavelmente não será muito grande. Podemos concluir que as árvores oferecem a mais alta eficiência para todas as operações de armazenamento de dados comuns. Árvores desequilibradas Uma árvore desequilibrada possui a maioria dos seus nós em um lado da raiz. As árvores tornam-se desequilibradas por causa da ordem na qual os itens de dados são inseridos. Se os itens são inseridos aleatoriamente, a árvore estará mais ou menos equilibrada. Entretanto, se uma seqüência ascendente ou descendente de dados for gerada, todos os valores serão filhos direitos ou filhos esquerdos, respectivamente. Se uma árvore é criada por itens de dados cujos valores-chave chegam em ordem aleatória, o problema das árvores desequilibradas pode não ser grande para árvores maiores, por que as chances de ocorrer uma longa execução dos números na seqüência são pequenas. Por outro lado, se os dados chegam em ordem, a eficiência da árvore poderá ser seriamente prejudicada. Existem métodos para resolver o problema das árvores desequilibradas, como as árvores red-black. 90 42 75 23 83 31 10 7 95 18 78 87 Árvore Desequilibrad a Árvore Binária Chamamos de Árvores Binárias (AB), um conjunto finito T de nós ou vértices, onde existe um nó especial chamado raiz e os restantes podem ser divididos em 2 subconjuntos disjuntos, chamados de sub-árvores esquerda e direita que também são Árvores Binárias. Em particular T pode ser vazio. Árvore binária é um tipo de estrutura de dados que, uma vez ordenada, permite pesquisa, inserção e exclusão de forma extremamente rápida. Exemplo: Cada nó numa arvore binária, pode ter então 0, 1 ou 2 filhos. Existe portanto uma hierarquia entre os nós. Com exceção da raiz, todo nó tem um nó pai. Dizemos que o nível da raiz é 1 e que o nível de um nó é o nível de seu pai mais 1. A altura de uma arvore binária é o maior dos níveis de seus nós.Dizemos que um nó é folha da arvore binária se não tem filhos. O tipo de árvore que estaremos vendo aqui é conhecido como árvore de procura binária. A característica que define uma árvore de procura binária é a seguinte: o filho esquerdo de um nó deve ter uma chave menor do que seu pai e o filho direito de um nó deve ter uma chave maior ou igual ao seu pai. 53 30 14 9 72 39 23 84 61 79 Os exemplos abaixo , mostram que podemos ter várias arvores binárias de busca (pesquisa) com os mesmos elementos , ou seja o objetivo é sempre termos uma arvores binárias de busca de menor altura. Nesse sentido a primeira arvores binárias de busca é melhor que a segunda. Árvores binárias como listas ligadas Podemos representar uma arvore binária de busca com uma lista ligada, onde cada elemento tem os seguintes campos: info - campo de informação eprox - apontador para a sub-árvore esquerda dprox - apontador para a sub-árvore direita A complexidade é a altura da árvore, portanto é conveniente que a árvore tenha sempre altura mínima. Pesquisa em uma Arvore Binária 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 (e) –raiz (r) –direita (d) . Na varredura e-r-d , 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. Inserção numa Arvore Binária Um novo elemento é inserido sempre como uma folha de uma arvore binária de busca. É necessário descer na arvore binária até encontrar o nó que será o pai deste novo nó. Remoção numa Arvore Binária A remoção é um pouco mais complexa que a busca ou inserção. O problema da remoção física de um nó é que é necessário encontrar um outro nó para substituir o removido, caso o nó a ser removido tenha filhos. 1) O nó a ser removido não tem filhos (folha) 2) O nó a ser removido tem filhos direito e esquerdo Os candidatos à substituto são obtidos percorrendo-se a arvore binária , um à esquerda e tudo a direita até achar nó com dprox NULL ou um a direita e tudo à esquerda até achar nó com eprox NULL. Além de alterar o ponteiro para o nó que vai ser substituído, é necessário mover o conteúdo deste nó para o nó a remover e fisicamente remover o substituto. O pai do substituto assume os seus filhos. Arvore Balanceada (AVL) 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. 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. Idealmente queremos que a árvore esteja balanceada, ou seja, para um nodo p qualquer, a altura da subárvore esquerda é aproximadamente igual à altura da subárvore direita. Obviamente há um custo extra de processamento para manter a árvore balanceada, mas que é compensado quando os dados armazenados precisam ser recuperados muitas vezes. A idéia de manter uma árvore binária balanceada dinamicamente, ou seja, enquanto os nodos estão sendo inseridos foi proposta em 1962 por 2 soviéticos chamados AdelsonVelskii e Landis. Este tipo de árvore ficou então conhecida como árvore AVL, pelas iniciais dos nomes dos seus inventores. Por definição uma árvore AVL é uma árvore binária de pesquisa onde a diferença em altura entre as subárvores esquerda e direita é no máximo 1 (positivo ou negativo). Assim, para cada nodo podemos definir um fator de balanceamento (FB), que vem a ser um número inteiro igual a FB(nodo p) = altura(subárvore direita p) - altura(subárvore esquerda p) Balanceamento em Arvores AVL Como fazemos então para manter uma árvore AVL balanceada? Inicialmente inserimos um novo nodo na árvore normalmente. A inserção deste novo nodo pode ou não violar a propriedade de balanceamento. Caso a inserção do novo nodo não viole a propriedade de balanceamento podemos então continuar inserindo novos nodos. Caso contrário precisamos nos preocupar em restaurar o balanço da árvore. A restauração deste balanço é efetuada através do que denominamos ROTAÇÕES na árvore Rotação Simples para Direita Rotação Simples para Esquerda Rotação Dupla para Direita: é composta de uma rotação simples à direita, seguida de uma rotação simples à esquerda. Rotação Dupla para Esquerda: é composta de uma rotação simples à esquerda, seguida de uma rotação simples à direita. Uma aplicação com Arvore Binária 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 Eficiência com Arvores Binárias A maioria das operações em árvores envolve descer até o nível mais baixo da árvore. Em uma árvore completa, metade dos nós está no nível inferior. Desta forma, cerca de metade de todas as procuras, inserções ou exclusões requerem encontrar um nó no nível mais baixo. Durante uma procura, visitamos um nó de cada nível. Percorrer uma árvore não é tão rápido, mas esta não é uma operação muito freqüente de se realizar num banco de dados grande comum. Um percurso é necessário na análise de expressões algébricas, mas neste caso a árvore provavelmente não será muito grande. Podemos concluir que as árvores oferecem a mais alta eficiência para todas as operações de armazenamento de dados comuns.