Prévia do material em texto
Árvores Estruturas de Dados Prof. Vilson Heck Junior INTRODUÇÃO Árvores Introdução • Árvores são estruturas de dados utilizadas para armazenar e recuperar dados de forma rápida e eficiente; • Árvores não são lineares, ou seja, os elementos não são dispostos em forma sequencial; Introdução • Árvores são amplamente utilizadas e possuem diversas estratégias de implementação; • Cada estratégia tem um conjunto específico de algoritmos para armazenamento e recuperação dos dados; – Árvores genealógicas são análogas às estruturas de dados árvores. COMPOSIÇÃO Árvores Composição • As árvores são compostas por: – Nodos/Nós: são os elementos inseridos em uma árvore; – Arestas: fazem a ligação entre 2 nodos; Composição • O primeiro Nodo de uma árvore é chamado de Raiz; – Todos os demais nodos da árvore serão seus descendentes; • Nodos que não tenham filhos serão chamados de folhas, ou terminais; • Os demais nodos são ditos intermediários, ou às vezes: galhos. Composição • As principais propriedades de cada nodo são: – Ordem/Grau: Indica o número de filhos que o nodo possui; – Nível/Altura/Profundidade: Indica a distância do nodo em relação à raiz; • A árvore também possuí valores de ordem e de nível. – Ambos os valores serão assumidos com base no maior valor encontrado em qualquer nodo. A CB D FE G IH J Nível 0 Nó A = Raiz – Ordem 2 Nível 1 Nó B = Galho – Ordem 3 Nó C = Folha – Ordem 0 Nível 2 Nó D = Folha – Ordem 0 Nó E = Galho – Ordem 4 Nó F = Folha – Ordem 0 Nível 3 Nó G = Folha – Ordem 0 Nó H = Folha – Ordem 0 Nó I = Folha – Ordem 0 Nó J = Folha – Ordem 0 Árvore: Nível 3 e Ordem 4 Nós / Nodos Arestas N íve l Ordem OUTRAS PROPRIEDADES Árvores Propriedades • Qualquer nodo de uma árvore pode ser chamado de raiz para uma sub-árvore; • Um Nodo Pai é um nodo conectado diretamente acima de outro nodo; • Um Nodo Filho é um nodo conectado diretamente abaixo de outro nodo; • Nodos Irmãos compartilham o mesmo nodo pai; Propriedades • Nodos Ancestrais são todos os nodos que estão acima de um nodo, com ligação direta ou indireta; • Nodos Descendentes são todos os nodos que estão abaixo de um nodo, com ligação direta ou indireta; EXERCÍCIOS Árvores A CB D F E G IH MK N O P TSR U X 765 # 4 & 3WY Z 8 9 ÁRVORES BINÁRIAS Árvores Árvores Binárias • Árvores Binárias são árvores com todas as propriedades vistas anteriormente, porém com algumas regras: – A Ordem máxima da árvore é 2; – Todos os nodos de uma sub-árvore, do filho à direita, serão maiores do que o nodo pai; – Todos os nodos de uma sub-árvore, do filho à esquerda, serão menores do que o nodo pai; • Geralmente chamadas de “Árvores binárias de pesquisa”; Árvores Binárias – O número máximo de nodos para cada nível de uma árvore binária é determinada pela equação: níveltotal 2 A CB D FE G IH J Nível 0 Nó A = Raiz – Ordem 2 Nível 1 Nó B = Galho – Ordem 2 Nó C = Galho – Ordem 2 Nível 2 Nó D = Folha – Ordem 0 Nó E = Galho – Ordem 2 Nó F = Folha – Ordem 0 Nó G = Folha – Ordem 0 Nível 3 Nó H = Galho – Ordem 1 Nó I = Folha – Ordem 0 Árvore Binária: Nível 4 e Ordem 2 Nós / Nodos Arestas N íve l Ordem Nível 4 Nó J = Folha – Ordem 0 EXEMPLO: ÁRVORE BINÁRIA EM MDA Árvores IMPLEMENTADO ÁRVORES BINÁRIAS Árvores Implementando • Árvores são implementadas com elementos dinâmicos, assim como nas listas encadeadas; • Nas listas, tínhamos apenas um próximo elemento. Nas árvores, cada elemento terá “dois próximos”: – Filho da Esquerda; e – Filho da Direita. Implementando public class Nodo { public int numero; //outros atributos //... public Nodo esquerda; public Nodo direita; public Nodo pai; //Opcional } Implementando • Para implementar operações em árvores, é necessário percorrer diversos nodos dela; – Para inserir um novo elemento, é necessário descer a árvore nível por nível até encontrar a posição adequada ao novo elemento; – Para remover um elemento, é necessário descer a árvore até encontrar o elemento a ser removido; Implementando • Vale lembrar das regras mais importantes de navegação nas árvores binárias de pesquisa: – Elementos menores estarão à esquerda; – Elementos maiores estarão à direita. Implementando • Inserção: – Se a raiz estiver vazia: • Insere na raiz. – Senão: • Navega pela árvore (subindo de nível) comparando o valor a ser inserido com os valores dos nodos visitados; – Se Menor – Esquerda; e – Se Maior – Direita; • Quando encontrar um nodo sem filho para o lado por onde deveria seguir, insere o novo elemento nesta posição. public static void inserir(Nodo novo) { novo.direita = null; novo.esquerda = null; novo.pai = null; if (raiz == null) { raiz = novo; } else { Nodo aux = raiz; while (aux != null) { if (novo.numero < aux.numero) { if (aux.esquerda == null) { aux.esquerda = novo; break; } aux = aux.esquerda; } else { if (aux.direita == null) { aux.direita = novo; break; } aux = aux.direita; } } novo.pai = aux; } } Implementando • Pesquisa: – Se a raiz estiver vazia: • Não encontrado! – Senão: • Navega pela árvore (subindo de nível) comparando o valor pesquisado com os valores dos nodos visitados: – Igual – Encontrado! – Menor – Desloca à Esquerda; – Maior – Desloca à Direita; – Se percorrer toda a árvore sem encontrar: • Não encontrado! Implementando public Nodo pesquisar(int nro) { Nodo aux = raiz; while (aux != null) { if (aux.numero == nro) { return aux; } else if (nro < aux.numero) { aux = aux.esquerda; } else { aux = aux.direita; } } return null; } Implementando • Navegar por todos os Nodos: 6 82 1 4 3 Implementando • Listar Conteúdo da Árvore: – A listagem da árvore pode ser feitas de três formas básicas: • Em ordem: – 1, 2, 3, 4, 6, 8 • Pré-ordem: – 6, 2, 1, 4, 3, 8 • Pós-ordem: – 1, 3, 4, 2, 8, 6 6 82 1 4 3 Implementando • Listar Conteúdo da Árvore: – Pode ser implementado com diversas estratégias: • Recursividade; • Pilha; • Busca do subsequente. 6 82 1 4 3 Listar com Recursividade public static void listar(Nodo inicio) { if (inicio == null) { return; } listar(inicio.esquerda); System.out.println(inicio.numero); listar(inicio.direita); } Implementando • Remoção de Nodos: – Aux = Pesquisar(Nodo) – Se Aux == NULL • não encontrado! – Senão: • Se Aux for raiz: – Sem filhos – Com um filho – Com dois filhos • Senão – Sem filhos – Com um filho – Com dois filhos Implementando • Remoção de Nodos: – Aux = Pesquisar(Nodo) – Se Aux == NULL • não encontrado! – Senão: • Se Aux for raiz: – Sem filhos – Com um filho – Com dois filhos • Senão – Sem filhos – Com um filho – Com dois filhos Implementando • Remoção de Nodos – Raiz sem filhos: – Raiz = null; Implementando • Remoção de Nodos: – Aux = Pesquisar(Nodo) – Se Aux == NULL • não encontrado! – Senão: • Se Aux for raiz: – Sem filhos – Com um filho – Com dois filhos • Senão – Sem filhos – Com um filho – Com dois filhos Implementando • Remoção de Nodos – Raiz com um filho: – Raiz = Filho Único da Raiz; Implementando • Remoção de Nodos: – Aux = Pesquisar(Nodo) – Se Aux == NULL • não encontrado! – Senão: • Se Aux for raiz: – Sem filhos – Com um filho – Com dois filhos • Senão – Sem filhos – Com um filho – Com dois filhos Implementando • Remoção de Nodos – Raiz com dois filhos: – Substituto = Subsequente(Raiz); //Aux – Substitui (Raiz pelo Substituto); Implementando • Remoção de Nodos: – Aux = Pesquisar(Nodo) – Se Aux == NULL • não encontrado! – Senão: • Se Aux for raiz: – Sem filhos – Com um filho – Com dois filhos • Senão – Sem filhos – Com um filho – Com dois filhos Implementando • Remoção de Nodos – Não Raiz sem filhos: – PaiAux = Aux.Pai; – Se (PaiAux.Esquerda == Aux) então • PaiAux.Esquerda = null; – Senão • PaiAux.Direita = null; Implementando • Remoção de Nodos: – Aux = Pesquisar(Nodo) – Se Aux == NULL • não encontrado! – Senão: • Se Aux for raiz: – Sem filhos – Com um filho – Com dois filhos • Senão – Semfilhos – Com um filho – Com dois filhos Implementando • Remoção de Nodos – Não Raiz / 1 filho: – PaiAux = Aux.Pai; – FilhoAux = Aux.Filho; (esquerda, ou direita) – Se (PaiAux.Esquerda == Aux) então • PaiAux.Esquerda = FilhoAux; – Senão • PaiAux.Direita = FilhoAux; Implementando • Remoção de Nodos: – Aux = Pesquisar(Nodo) – Se Aux == NULL • não encontrado! – Senão: • Se Aux for raiz: – Sem filhos – Com um filho – Com dois filhos • Senão – Sem filhos – Com um filho – Com dois filhos Implementando • Remoção de Nodos – Não Raiz / 2 filhos: – Substituto = Subsequente(Aux); – Substitui (Aux pelo Substituto); Implementando private static boolean substituirNodo(Nodo original, Nodo novo) { if (original == null || novo == null) { return false; } Nodo pai = original.pai; if (pai != null) { if (pai.esquerda == original) { pai.esquerda = novo; } else { pai.direita = novo; } } novo.pai = pai; Nodo filho = original.esquerda; if (filho != null) { filho.pai = novo; } novo.esquerda = filho; filho = original.direita; if (filho != null) { filho.pai = novo; } novo.direita = filho; return true; } Continuação... Implementando (1/3) public static boolean remover(Nodo rem) { if (rem == null) { return false; } //Informação paterna Nodo pai = rem.pai; //Existe um substituto único ou nenhum? Nodo subst; if (rem.esquerda == null || rem.direita == null) { //Ambos são null? sem substituto if (rem.esquerda == rem.direita) { subst = null; } else { //Se direita vazia, substituto esta na esquerda e vice versa if (rem.direita == null) { subst = rem.esquerda; } else { subst = rem.direita; } } Implementando (2/3) //Se tiver um pai, atualiza vinculo dos filhos, senão atualiza a raiz. if (pai != null) { if (pai.esquerda == rem) { pai.esquerda = subst; } else { pai.direita = subst; } } else { raiz = subst; } //Se tiver um filho substituto, atualiza seu vinculo paterno if (subst != null) { subst.pai = pai; } return true; } Implementando (3/3) //Se chegarmos aqui é por que o nodo excluído tem dois filhos. (Subsequente) //Procuramos o elemento imediatamente maior e que não tenha filho a esquerda Nodo proximo = rem.direita; while (proximo.esquerda != null) { proximo = proximo.esquerda; } //Removemos o nodo que tem apenas um filho remover(proximo); //Recursividade //Inserimos o nodo "próximo removido" no lugar do "removido" substituirNodo(rem, proximo); //Se estivermos excluindo a raiz, atualiza ela if (pai == null) { raiz = proximo; } return true; } Trabalho • Implemente um Árvore Binária de Pesquisa para realizar a inserção, pesquisa, remoção e listagem de um cadastro de alunos. Os alunos serão cadastrados por: – Nome (chave, String) – Matricula (long) – Nome do curso (String) Como nome é o campo chave, a separação esquerda/direita dos elementos deverá ser feita com base nos nomes dos alunos. Exemplo de função para comparar strings de forma “alfabética”: ÁRVORES BALANCEADAS Árvores Imagem: Natalie Jeremijenko Árvores • O pior cenário para uma árvore binária de pesquisa é quando uma sequência de números é inserida já em ordem: – Crescente; ou – Decrescente. Árvore não Balanceada • Entrada: 2, 5, 8, 10, 15 2 5 8 10 15 Árvores • Para melhorar o tempo de busca em árvores binárias de pesquisa, é possível aplicar técnicas de balanceamento, conforme ilustração a seguir: Balanceando a Árvore 2 5 8 10 15 Balanceando a Árvore 2 5 8 10 15 Balanceando a Árvore 2 5 8 10 15 Árvores AVL • O conceito de Árvore AVL foi criado em 1962 por Adelson-Velsky e Landis; • É uma árvore binária de pesquisa balanceada; • O primeiro diferencial: – cada nodo possuí um campo numérico indicando a diferença de altura para cada subárvore filha: • 0 : subárvores com mesma altura; • 1 : subárvore direita é 1 nível maior; • -1 : subárvore esquerda é 1 nível maior; A CB D E IH Nível 0 Nó A = Raiz – Ordem 2 Nível 1 Nó B = Galho – Ordem 2 Nó C = Galho – Ordem 2 Nível 2 Nó D = Galho – Ordem 2 Nó E = Galho – Ordem 1 Nível 3 Nó H = Folha – Ordem 0 Nó I = Folha – Ordem 0 Árvore Binária NÃO AVL: Nível 3 e Ordem 2 Nós / Nodos Arestas N íve lOrdem Nível 4 A CB D E IH Nível 0 Nó A: Dif = -2 Nível 1 Nó B: Dif = -1 Nó C: Dif = 0 Nível 2 Nó D: Dif = 0 Nó E: Dif = 0 Nível 3 Nó H: Dif = 0 Nó I: Dif = 0 Diferenças Árvores AVL • Principal Regra: – Nenhum nodo pode permanecer com diferença maior de nível em qualquer subárvore maior do que 1: • 2 • – 2 – Sempre que esta condição não for satisfeita, devem ocorrer rotações na árvore, até que ela esteja balanceada, ou seja, com qualquer subárvore A CB D E IH Nível 0 Nó A: Dif = -2 Nível 1 Nó B: Dif = -1 Nó C: Dif = 0 Nível 2 Nó D: Dif = 0 Nó E: Dif = 0 Nível 3 Nó H: Dif = 0 Nó I: Dif = 0 Alguma |diferença| > 1 ? Árvores AVL • Rotações: Diferença Diferença do filho Tipo de Rotação 2 1 Simples: à esquerda 0 Simples: à esquerda -1 Dupla: filho para a direita e pai para a esquerda -2 1 Dupla: filho para a esquerda e pai para a direita 0 Simples: à direita -1 Simples: à direita A CB D E IH Nível 0 Nó A: Dif = -2 Nível 1 Nó B: Dif = -1 Nó C: Dif = 0 Nível 2 Nó D: Dif = 0 Nó E: Dif = 0 Nível 3 Nó H: Dif = 0 Nó I: Dif = 0 Diferença do Filho Maior Árvores AVL • Rotações: Diferença Diferença do filho Tipo de Rotação 2 1 Simples: à esquerda 0 Simples: à esquerda -1 Dupla: filho para a direita e pai para a esquerda -2 1 Dupla: filho para a esquerda e pai para a direita 0 Simples: à direita -1 Simples: à direita A CB D E IH Rotação à Direita A CB D E IH Rotação à Direita A C B D E IH Rotação à Direita A C B D E IH Rotação à Direita A C B D E IH Rotação à Direita A C B D E IH Rotação à Direita A C B D EIH Árvore Binária AVL: Nível 2 e Ordem 2 Nível 0 Nó B: Dif = 0 Nível 1 Nó D: Dif = 0 Nó A: Dif = 0 Nível 2 Nó H: Dif = 0 Nó I: Dif = 0 Nó E: Dif = 0 Nó C: Dif = 0 Nível 3 N íve l Ordem ROTAÇÕES Árvores AVL Rotação Simples à Esquerda private boolean RotacaoEsquerda(Nodo centro) { if (centro == null) return false; Nodo aux = centro.Direita; if (aux == null) return false; //Atualizar a raiz? if (centro == raiz) raiz = aux; //Troca os pais aux.Pai = centro.Pai; centro.Pai = aux; if (aux.Pai != null) { Nodo pai = aux.Pai; if (pai.Esquerda == centro) pai.Esquerda = aux; else pai.Direita = aux; } //Troca os filhos Nodo troca = aux.Esquerda; aux.Esquerda = centro; centro.Direita = troca; return true; } Rotação Simples à Direita private boolean RotacaoDireita(Nodo centro) { if (centro == null) return false; Nodo aux = centro.Esquerda; if (aux == null) return false; //Atualizar a raiz? if (centro == raiz) raiz = aux; //Troca os pais aux.Pai = centro.Pai; centro.Pai = aux; if (aux.Pai != null) { Nodo pai = aux.Pai; if (pai.Esquerda == centro) pai.Esquerda = aux; else pai.Direita = aux; } //Troca os filhos Nodo troca = aux.Direita; aux.Direita = centro; centro.Esquerda = troca; return true; } Rotação Dupla à Esquerda private boolean RotacaoDuplaEsquerda(Nodo centro) { if (centro == null) return false; Nodo aux = centro.Direita; if (aux == null) return false; boolean retorno = RotacaoDireita(aux); if (retorno == false) return false; return RotacaoEsquerda(centro); } Rotação Dupla à Direita private boolean RotacaoDuplaDireita(Nodo centro) { if (centro == null) return false; Nodo aux = centro.Esquerda; if (aux == null) return false; boolean retorno = RotacaoEsquerda(aux); if (retorno == false) return false; return RotacaoDireita(centro); } OUTRAS ALTERAÇÕES Árvores AVL Alterações Classe Nodo • Novos atributos: public int AltEsq; public int AltDir; public int Dif; • Construtor: this.Dif = 0; this.AltDir = 0; this.AltEsq = 0; AlturaSubArvore – Calculo Dif public int AlturaSubArvore(Nodo atual) { if (raiz == null) return 0; if (atual == null) atual = raiz; if (atual.Esquerda ==null) atual.AltEsq = 0; else atual.AltEsq = AlturaSubArvore(atual.Esquerda) + 1; if (atual.Direita == null) atual.AltDir = 0; else atual.AltDir = AlturaSubArvore(atual.Direita) + 1; atual.Dif = atual.AltDir - atual.AltEsq; if (atual.AltDir > atual.AltEsq) return atual.AltDir; else return atual.AltEsq; } Balanceamento da Árvore (1/2) public void BalancearArvore(Nodo atual) { if (raiz == null) return; if (atual == null) atual = raiz; if (atual.Dif > 1) { if (atual.Direita.Dif == -1) RotacaoDuplaEsquerda(atual); else RotacaoEsquerda(atual); return; } Balanceamento da Árvore (2/2) if (atual.Dif < -1) { if (atual.Esquerda.Dif == 1) RotacaoDuplaDireita(atual); else RotacaoDireita(atual); return; } if (atual.Esquerda != null) BalancearArvore(atual.Esquerda); if (atual.Direita != null) BalancearArvore(atual.Direita); }