Estruturas de Dados II Rodrigo Porfírio da Silva Sacchi rodrigosacchi@ufgd.edu.br 3410-2086 Aula 2: Árvores http://www.do.ufgd.edu.br/rodrigosacchi
Árvores
Definição: Árvores Uma árvore T é um conjunto finito de elementos denominados nós ou vértices, tal que: T = 0 e a árvore é dita vazia; ou Existe um nó raiz de T, os nós restantes podem ser divididos em n subconjuntos disjuntos, que são as subárvores de T, e as quais, por sua vez, também são árvores.
Exemplo Raiz 5 3 7 2 4 8 Isto é, um nó de uma árvore pode possuir n subárvores.
Definição: Árvores Binárias Uma árvore binária T é um conjunto finito de elementos denominados nós ou vértices, tal que: T = 0 e a árvore é dita vazia; ou Existe um nó raiz de T, os nós restantes podem ser divididos em dois subconjuntos disjuntos, árvore esquerda e árvore direita, que são as subárvores esquerda e direita de T, e as quais, por sua vez, também são árvores binárias.
Representação Raiz 5 3 7 2 4 8 Árvore esquerda Árvore direita
Representação Raiz 2 3 7 5 8 4
Algumas definições A raiz de uma árvore é chamada de pai de suas subárvores; Pai subárvore esquerda subárvore direita
Algumas definições Como temos uma definição recursiva, todo nó é pai de duas árvores (pode ser vazia); Pai Pai (árvore direita vazia) Subárvore esquerda Subárvore direita Pai (duas árvores vazias)
Algumas definições Nós com o mesmo pai são denominados irmãos. Irmãos Irmãos
Algumas definições O grau de um nó é o número de subárvores de um nó. 2 2 1 0 0 1 0
Algumas definições Um nó sem subárvores é denominado um nó folha, ou seja, um nó com grau igual a zero.
Algumas definições O comprimento de um caminho desde a raiz R até um nó N denomina-se o nível do nó N. Nível 1 Nível 2 Nível 3 Nível 4
Algumas definições O maior nível de uma árvore é denominado a altura ou profundidade da árvore. Nível 4
Propriedade de Árvores Binárias Seja x um nó em uma árvore de pesquisa binária; Se y é um nó na subárvore esquerda de x, então chave[y] <= chave[x]; Se y é um nó na subárvore direita de x, então chave[x] <= chave[y]. Esta propriedade nos permite imprimir todas as chaves de uma árvore binária em uma seqüência ordenada.
Definição da estrutura Chamaremos a estrutura de nó; Para uma variável do tipo nó referenciada por meuno tem-se: meuno Pai Ponteiro para o nó Pai; meuno Dado Informação armazenada; meuno Esq Ponteiro para a subárvore esquerda; meuno Dir Ponteiro para a subárvore direita.
Definição da Estrutura Dado Pai Esq Dir
Definição da Estrutura declare No registro { dado: inteiro; esq: ponteiro No; dir: ponteiro No; pai: ponteiro No; }; Cada elemento em uma árvore é representado basicamente por estes quatro campos. declare raiz: ponteiro No;
Operações em árvores binárias Se p é um ponteiro para o nó n de uma árvore binária, e sendo x do tipo do dado do nó e q do tipo ponteiro para nó: x = Dado(p): retorna o conteúdo de n. q = Esq(p)/Dir(p): retorna ponteiro para o filho esquerdo/direito de n; retorna NULL se Esq ou Dir não existir.
Operações em árvores binárias p = CriaArvore(x): cria nova árvore com um único nó com o campo informação valendo x, e retorna um ponteiro para aquele nó; CriaNoEsq(p,x)/CriaNoDir(p,x): recebe ponteiro para nó sem filho esquerdo/direito e cria novo nó, filho esquerdo/direito do nó apontado por p com Dado valendo x. Podemos criar uma única função para alocar um nó de uma árvore, ao invés de criar três funções.
Percurso em Árvores Binárias Três tipos de percursos: Percurso de árvore em ordem: A chave da raiz da subárvore é impressa entre os valores de sua subárvore esquerda e aqueles de sua subárvore direita. Percurso de árvore em pré-ordem: Imprime a raiz antes dos valores de uma ou outra subárvore. Percurso de árvore em pós-ordem: Imprime a raiz depois dos valores contidos em suas subárvores.
Percurso em Árvores Binárias Percurso de árvore em ordem: procedimento emordem(ptr: ponteiro No) início se ( ptr!= null ) então emordem(ptr esq); escreva ptr dado; emordem(ptr dir); fim_se fim
Percurso em Árvores Binárias Percurso de árvore em pré ordem: procedimento preordem(ptr: ponteiro No) início se ( ptr!= null ) então escreva ptr dado; preordem(ptr esq); preordem(ptr dir); fim_se fim
Percurso em Árvores Binárias Percurso de árvore em pós ordem: procedimento posordem(ptr: ponteiro No) início se ( ptr!= null ) então posordem(ptr esq); posordem(ptr dir); escreva ptr dado; fim_se fim
Percurso em Árvores Binárias Complexidade: Dado n o número de nós em uma árvore binária de busca, os três algoritmos gastam Θ(n); Por que? Após a chamada inicial, o procedimento é chamado de forma recursiva exatamente duas vezes para cada nó na árvore, uma vez para seu filho da esquerda e uma vez para seu filho da direita.
Busca em uma Árvore Binária Operação mais comum a ser executada por uma chave armazenada na árvore: função ponteiro No buscaarvore(ptr: ponteiro No, k, inteiro) início se ( ptr = null k = ptr dado ) então retorne ptr; fim_se se ( k < ptr dado ) então retorne buscaarvore(ptr esq, k); senão retorne buscaarvore(ptr dir, k); fim_se fim
Busca em uma Árvore Binária 15 6 18 3 7 17 20 2 4 13 9
Busca em uma Árvore Binária Os nós encontrados durante a recursão formam um caminho descendente na árvore, a partir da raiz; Portanto, o tempo de execução de buscaarvore é O(h), onde h é a altura da árvore. Exercício: Como podemos transformar esta função em uma função iterativa?
Mínimo e Máximo O menor elemento em uma árvore binária pode ser encontrado percorrendo a mesma usando ponteiros para os filhos da esquerda desde a raiz até o elemento ser encontrado. De forma análoga, o maior elemento em uma árvore binária pode ser encontrado percorrendo a mesma usando ponteiros para os filhos da direita desde a raiz até o elemento ser encontrado.
Mínimo e Máximo A função a seguir retorna um ponteiro para o elemento mínimo na subárvore com raiz em um determinado nó ptr. função ponteiro No minimo(ptr: ponteiro No) início enquanto ( ptr esq!= null ) faça ptr ptr esq; fim_enquanto retorne ptr; fim
Mínimo e Máximo Exercício: Implemente uma função que retorne um ponteiro para o maior elemento em uma árvore binária. Ambos são executados em um tempo O(h) em uma árvore de altura h.
Sucessor e Predecessor Dado um nó de uma árvore binária, às vezes é importante ser capaz de encontrar seu sucessor na seqüência ordenada determinada por um percurso de árvore em ordem; Se todas as chaves são distintas, o sucessor de um nó ptr é o nó com a menor chave maior que a chave de ptr.
Sucessor e Predecessor A estrutura de uma árvore binária nos permite descobrir o sucessor de um nó sem sequer comparar chaves; A função Sucessor retorna um ponteiro para o sucessor de ptr ou NULL se ptr tem a maior chave na árvore.
Sucessor e Predecessor função ponteiro No sucessor(ptr: ponteiro No) declare aux: ponteiro No; início se ( ptr dir!= null ) então retorne minimo(ptr dir); fim_se aux ptr pai; enquanto ( aux!= null && ptr = aux dir ) faça ptr aux; aux aux Pai; fim_enquanto retorne aux; fim
Sucessor e Predecessor Primeiro caso: Se a subárvore direita do nó ptr for não vazia, o sucessor de ptr será exatamente o nó da extremidade esquerda na subárvore direita.
Sucessor e Predecessor Segundo caso: Se a subárvore direita do nó ptr for vazia e ptr tiver um sucessor aux, então aux será o ancestral mais baixo de ptr cujo filho da esquerda também é um ancestral de ptr; Na Figura a seguir, o sucessor de 15 é 17, enquanto o sucessor de 13 é 15
Sucessor e Predecessor 15 6 18 3 7 17 20 2 4 13 9
Sucessor e Predecessor Complexidade: Tempo de execução de Sucessor em uma árvore de altura h é O(h), pois segue-se um caminho para cima na árvore, ou um caminho para baixo na árvore. Predecessor é simétrico de Sucessor e também é executado em um tempo O(h).
Sucessor e Predecessor Exercício: Implemente a função Predecessor, a qual retorna um ponteiro para um tipo No e passa como argumento um ponteiro para No.
Inserção Assim como feito para listas encadeadas, para inserir um nó em uma árvore binária, este novo nó deve ser alocado dinamicamente usando a função malloc. Deve existir uma função para alocar um nó em uma árvore.
Inserção Para alocar um nó de uma árvore binária: procedimento aloca(novo: ponteiro No, dado: inteiro) início novo new No(); // Misturando com Java novo pai null; novo esq null; novo dir null; novo dado dado; fim
Inserção Para inserir um valor v, utilizamos a função abaixo, a qual insere um novo nó na árvore, mantendo suas propriedades A função passa como argumento um ponteiro para a raiz da árvore e um valor do tipo int, para ser colocado como campo chave do novo nó.
Inserção função lógico insere(ptr: ponteiro No, valor: inteiro, pai: ponteiro No) início se ( ptr = null ) então // Árvore inicialmente vazia. aloca(ptr, valor); ptr pai pai; senão // Arvore não está vazia. se ( valor < ptr dado ) então insere(ptr esq, valor, ptr); senão se ( valor > ptr dado ) então insere(ptr dir, valor, ptr); senão escreva Elemento já existe"; fim_se fim_se fim_se fim
Inserção 12 5 18 2 9 15 19 13 17 Qual o tempo de execução desta função?
Remoção Devem ser considerados três casos: Se o nó a ser removido não tem filhos, modifica o pai para apontar para NULL. 13 13 5 17 5 17 2 9 19 2 19
Remoção Devem ser considerados três casos: Se o nó tem um único filho, extraí o nó criando um novo vínculo entre sei pai e seu filho. 13 13 5 17 5 19 2 9 19 2 9
Remoção Devem ser considerados três casos: Finalmente, se o nó z (a ser removido) tem dois filhos, extrai-se y, o sucessor do nó, que não tem nenhum filho da esquerda e substitui-se dados e ponteiros de z pelos de y.
Remoção 13 13 5 17 9 17 2 9 19 2 19 Qual o tempo de execução desta função?
Desaloca Crie uma função recursiva que desaloque todos os nós da árvore. procedimento desaloca(ptr: ponteiro No) início se ( ptr!= null ) então desaloca(ptr esq); desaloca(ptr dir); delete ptr; ptr null; fim_se fim
Exercício 1 Implemente a uma função para imprimir, no vídeo, os elementos de uma árvore binária, mantendo as características de representação de uma árvore binária.
Exercício 2 Implemente a remoção em uma árvore binária. Fica apenas como exercício, visto que implementações de árvores mais importantes estão por vir. Realize uma pesquisa sobre altura de uma árvore e implemente uma função que calcule a altura de uma árvore.
Imprime em Nível procedimento imprime (ptr: ponteiro No, nivel: inteiro) início declare k: inteiro; se ( ptr!= null ) então imprime (ptr dir, nivel +1); para k de 0 até k < 3 + nivel faça escreva " "; // Imprima espaço em branco. escreva ptr -> dado; // Pule uma linha antes. imprime (ptr esq, nivel +1); fim_se fim
Algoritmo da Remoção função ponteiro No remover(ptr: ponteiro No, x: inteiro) início se( ptr == null ) então return null; senão se( k < ptr chave ) então retorne remover(ptr esq, x); senão se( k > ptr chave ) então retorne remover(ptr dir, x); senão retorne delete(ptr, x); fim_se fim_se fim_se fim
Algoritmo da Remoção Função ponteiro No delete(ptr: ponteiro No, x: inteiro) início se( ptr esq == null ou ptr dir == null ) então pty ptr; senão pty sucessor(ptr); fim_se se( pty esq!= null ) então ptx pty esq; senão ptx pty dir; fim_se se( ptx!= null ) então ptx pai pty pai; fim_se
Algoritmo da Remoção //... continuação se( pty pai == null ) então // Faça ptx ser raiz da árvore. senão se( pty == pty pai esq ) então pty pai esq ptx; senão pty pai dir ptx; fim_se fim_se se( pty!= ptr ) então ptr chave pty chave; // Se tiver mais dados, copiar. fim_se return pty; fim