Prévia do material em texto
ESTRUTURA DE DADOS EM JAVA OBJETIVOS DE APRENDIZAGEM > Definir árvore binária, suas propriedades e como percorrê-la. > Reconhecer as operações básicas de árvores binárias. > Exemplificar a utilização de árvores binárias em Java. Introdução Árvores estão entre as estruturas de dados mais importantes da ciência da com- putação, pois são fáceis de implementar e possuem um grande poder expressivo: são ideais para representar relações de hierarquia, subordinação, composição, organização e ordenação entre objetos. Além disso, árvores são estruturas de dados mais eficientes do que listas, principalmente quando comparamos as operações de inserção e consulta aos dados. Neste capítulo, apresentaremos as árvores binárias. Inicialmente, conhecere- mos a terminologia básica desse tipo de estrutura de dados e suas propriedades principais. Na sequência, estudaremos alguns algoritmos de manipulação desse tipo de árvore e entenderemos seu funcionamento. Finalmente, implementare- mos esse tipo abstrato de dados na linguagem Java, utilizando o paradigma de programação orientada a objetos. Árvores binárias em Java Lucas Rafael Costella Pessutto Árvores binárias Árvores são estruturas de dados não lineares, o que signifi ca que cada nodo de uma árvore possui apontadores para dois (ou mais) nodos. Diferentemente de listas, que são estruturas lineares, não há a noção de antes e depois entre os elementos de uma árvore, mas uma relação hierárquica entre seus nodos, que se encontram em diferentes níveis nessa hierarquia (GOODRICH; TAMASSIA, 2013). Um exemplo clássico de árvore é a organização dos diretórios e arquivos em um sistema operacional, como mostrado na Figura 1. A origem do sistema de arquivos é o diretório-raiz e, a partir dele, ramificam-se todos os demais diretórios; ou seja, a partir da raiz, podemos alcançar qualquer arquivo pre- sente nesse sistema operacional apenas descendo na hierarquia de diretórios. Figura 1. Árvore de arquivos de um sistema operacional. Fonte: Tanenbaum e Woodhull (2008, p. 455). Há diversos outros exemplos de árvores em nosso cotidiano, como uma árvore genealógica, um organograma de uma empresa e a organização de um livro, com seu título no nível hierárquico superior, seus capítulos no segundo nível e as seções em um nível abaixo deste, ligadas a seu respectivo capítulo. Nomenclatura e propriedades Árvores possuem uma nomenclatura especial, com inspiração biológica. Portanto, costumamos nos referir aos nodos como “raiz” e “folhas”. Também é comum utilizar as relações parentais para categorizar as posições relativas entre dois nodos da árvore: pai/mãe e fi lho, nodos irmãos, avô e neto, etc. Árvores binárias em Java2 O primeiro nodo de uma árvore é chamado de raiz. Como mencionado anteriormente, todo nodo de uma árvore aponta para dois ou mais nodos, chamados de nodos filhos, e cada nodo filho tem um único pai (DEITEL; DEITEL, 2017). Discutiremos, neste capítulo, somente as árvores binárias, que são aquelas cujo cada nodo possui no máximo dois filhos, um à esquerda e outro à direita. A Figura 2 contém um exemplo de árvore binária. Sua raiz é o nodo G e seus filhos são os nodos D (à esquerda) e J (à direita). Como os nodos D e J estão no mesmo nível da árvore, dizemos que esses nodos são irmãos. Outra forma de se referir a esses nodos é como raízes das subárvores (direita e esquerda) de G. Quando um nodo não possui filho, o chamamos de folha. Há cinco nodos folha na árvore da Figura 2: A, C, F, I e K. Figura 2. Exemplo de árvore binária. A altura de uma árvore consiste na quantidade de níveis em que a árvore foi construída. A altura da árvore da Figura 2 é quatro, pois ela contém quatro níveis: o primeiro, com a raiz (G), o segundo, com os filhos da raiz (D e J), o terceiro, com os nodos B, E, I e K, e, por fim, o quarto nível, com os nodos A, C e F. O grau de um nodo é a quantidade de filhos que possui. Por exemplo, o nodo G tem grau dois, o nodo E tem grau um e o nodo A tem grau zero. Por ser uma árvore binária, não há qualquer nodo com grau maior do que dois (FERRARI et al., 2014). Como árvores são estruturas não lineares, diversos caminhos podem ser percorridos a partir da raiz. Feofiloff (2008) define um caminho em uma árvore como uma sequência de nodos (a0, a1, ..., an), onde an+1 é filho de an e n é o tamanho do caminho. Um possível caminho da árvore da Figura 2 é (G, D, E, F). Esse caminho tem tamanho quatro. Um caminho em uma árvore Árvores binárias em Java 3 que percorre todos seus nodos uma única vez é chamado de caminhamento. Existem três tipos de caminhamentos: prefixado, pós-fixado e em ordem. O que difere em cada um desses caminhamentos é a forma como percorrem os nodos da árvore (EDELWEISS; GALANTE, 2008). Vejamos, a seguir, como cada um desses caminhos funciona. � Caminhamento prefixado: nesse caminhamento, é visitada a raiz da árvore, depois a subárvore da esquerda e, em seguida, a subárvore da direita. Considerando a árvore da Figura 2, o caminhamento prefixado visitaria os nodos na seguinte ordem: G–D–B–A–C–E–F–J–I–K. � Caminhamento pós-fixado: a ordem de visita dos nodos no caminha- mento pós-fixado consiste em visitar inicialmente a subárvore da esquerda, depois a subárvore da direita e, por fim, a raiz. Na árvore da Figura 2, o caminhamento pós-fixado produziria a seguinte sequência de nodos: A–C–B–F–E–D–I–K–J–G. � Caminhamento em ordem: o caminhamento em ordem visita, inicial- mente, a subárvore da esquerda, depois visita a raiz e, finalmente, visita a subárvore da direita. No exemplo da Figura 2, o caminhamento em ordem naquela árvore seria: A–B–C–D–E–F–G–I–J–K. Note que a definição de caminhamentos é recursiva. Portanto, quando visitamos uma de suas subárvores, esta é tratada como uma árvore também. Por exemplo, na árvore da Figura 2, ao visitar a subárvore esquerda da raiz G, obtemos uma árvore com raiz em D e, da mesma forma, ao visitar a subárvore esquerda do nodo D, obtemos uma nova árvore com raiz em B. Como veremos mais adiante neste capítulo, a maioria dos algoritmos que operam sobre árvores é escrita de maneira recursiva: acostume-se a enxergar árvores dessa forma. Operações sobre uma árvore binária Nesta seção, vamos ver como as operações básicas de uma estrutura de dados, a inserção e a remoção de elementos, podem ser implementadas em uma árvore binária. Consideraremos que as árvores tratadas são ordenadas, isto é, que existe uma relação de ordem para suas subárvores (EDELWEISS; GALANTE, 2008). Ainda, utilizaremos um tipo de árvore binária chamado de árvore binária de busca para definir tais operações. Além de ser bastante utilizado, esse tipo de árvore possui uma regra de ordenação simples: todo nodo deve possuir Árvores binárias em Java4 uma chave de pesquisa, que permite comparar os nós da árvore. Ao inserir um valor na árvore, o nodo é comparado com a raiz e é inserido na subárvore da esquerda, se for menor do que a raiz. Caso sua chave possua um valor maior do que o da raiz, nodo é inserido na subárvore da direita (FEOFILOFF, 2008). A árvore mostrada na Figura 2 é considerada um exemplo de árvore binária de busca, considerando a ordem lexicográfica para comparar as letras que foram inseridas. Veja como os nodos da subárvore da esquerda do nodo G somente possuem elementos menores do que G e como o oposto ocorre na subárvore da direita. Dado esse critério de ordenação, realizar pesquisas na árvore torna-se bastante eficiente. Por exemplo, imagine que queremos en- contrar o nodo E na árvore da Figura 2. Começando na raiz da árvore, sabemos que esse nodo, se estiver na árvore, estará na subárvore da esquerda, pois E < G. Prosseguindo a busca nessa subárvore, comparamos E com D. Nesse caso, concluímos que E deve estar na subárvore da direita de D, pois E > D, que é onde vamos encontrar o nodo contendo o valor E. Vamos, agora, definir como um nodo de uma árvore binária deve ser cons- truído. Por ser uma árvore binária, devemosincluir dois apontadores, um para a subárvore da esquerda e outro para a subárvore da direita, de modo a criar o encadeamento entre os nodos, conforme mostra a Figura 3. Além disso, cada nodo conterá uma seção para armazenar dados. Destacamos, aqui, o campo chave do nodo, que é obrigatório para árvores binárias de busca. Outros campos de dados podem ser adicionados, de acordo com a necessidade. Figura 3. Nodo de uma árvore binária. Inserção de nodos em uma árvore binária Para inserir um novo nodo em uma árvore binária, precisamos, inicialmente, testar se a raiz da árvore está vazia. Se estiver, alocamos o novo nodo como a raiz da árvore. Caso a raiz exista, comparamos sua chave com o valor da chave do novo nodo. Se a chave do novo nodo for menor do que a raiz e ela ainda não possuir um fi lho na subárvore da esquerda, encadeamos o novo Árvores binárias em Java 5 nodo na subárvore da esquerda. Se o valor da chave do novo nodo for maior do que o da raiz e se a raiz ainda não possuir um filho na subárvore da direita, então o novo nodo será encadeado nessa subárvore. Caso exista um filho na subárvore em que será feita a inserção, o filho se torna a raiz da árvore e repete-se, recursivamente, o processo de decidir se a inserção acontecerá na subárvore da direita ou da esquerda, até encontrar um local onde deva ser inserido o novo nodo. Veja, abaixo, um algoritmo recursivo que insere um novo nodo na árvore binária: função insere(Nodo raiz, int valor){ se (raiz == NULL){ Nodo novo = Nodo(valor) raiz = novo; } senao se (raiz.chave > valor){ se (raiz.filhoEsquerdo == NULL){ Nodo novo = Nodo(valor) raiz.filhoEsquerdo = novo } senao { insere(raiz.filhoEsquerdo, valor) } } senao se (raiz.chave < valor){ se (raiz.filhoDireito == NULL){ Nodo novo = Nodo(valor) raiz.filhoDireito = novo } senao { insere(raiz.filhoDireito, valor) } } } Árvores binárias em Java6 Para exemplificar, considere uma árvore que está inicialmente vazia. Agora, vamos realizar a inserção do nodo com chave 5 nessa árvore. Como ela está vazia, esse nodo será a raiz. Na sequência, se formos inserir o nodo 3, este será alocado à esquerda do nodo 5, pois é menor. Inserindo o valor 2 na árvore, necessita-se comparar 2 com 5. Como 2 é menor, ele será inserido na subárvore da esquerda, mas, como já existe um nodo nessa subárvore, agora comparamos o nodo 2 com o nodo 3. Como 2 também é menor, inserimos esse nodo na subárvore da esquerda do nodo 3. Seguindo essa regra, podemos inserir o nodo 4 à direita do nodo 3, o nodo 8 à direita do nodo 5, o nodo 6 à esquerda do nodo 8 e, por fim, o nodo 7 à direita do nodo 6. A árvore resultante dessas inserções pode ser vista na Figura 4. Figura 4. Árvore binária resultante da inserção dos nodos 5–3–2–4–8–6–7, nessa ordem. Há duas coisas que são interessantes de se observar na inserção de nodos em uma árvore binária. A primeira delas é que os novos nodos são sempre inseridos nas folhas da árvore. Logo, chamamos o método inserir recursi- vamente até encontrar um nodo que não tenha filhos para fazer a inserção. Também é interessante destacar que a ordem de inserção dos nodos em uma árvore influencia sua aparência. Por exemplo, se repetirmos o exemplo anterior, mas inserindo os nodos na ordem contrária na árvore, esta teria uma aparência diferente. Veja, na Figura 5, como ficaria essa árvore. Árvores binárias em Java 7 Figura 5. Árvore binária resultante da inserção dos nodos 7–6–8–4–2–3–5, nessa ordem. Perceba como a mudança na ordem de inserção modifica a aparência da árvore. Remoção de nodos em uma árvore binária A remoção de um nodo da árvore binária é mais complexa do que a inserção, pois devemos garantir que, após a remoção, a árvore se manterá ordenada, e por isso temos uma quantidade maior de casos que precisam ser conside- rados. O primeiro passo é buscar, na árvore, o valor que se deseja remover. Após, analisamos em qual dos casos a seguir ele se encaixa e realizamos a deleção do nodo. O caso mais simples de remoção é quando há um único nodo na árvore, bastando somente setar a raiz da árvore com o valor nulo. Quando a árvore possui um nodo filho, três situações distintas podem ocorrer no momento da remoção, de acordo com o grau do nodo que será removido. Vejamos. � Remoção de nodo de grau zero: remover um nodo folha (grau zero) é simples; basta encontrar seu pai e setar sua referência no nodo pai para nulo. � Remoção de nodo de grau um: quando o nodo que será removido possui uma subárvore, seu filho passa a ocupar seu local na árvore; ou seja, encadeamos o nodo avô com seu neto, removendo o nodo intermediário. � Remoção de nodo de grau dois: quando ambas as subárvores do nodo que será removido estão ocupadas, existem dois nodos que podem Árvores binárias em Java8 ser escolhidos para substituir o nodo que será removido, (1) o maior valor da subárvore da esquerda e (2) o menor valor da subárvore da direita. Escolher um desses dois nodos garantirá que a árvore conti- nue ordenada. Por convenção, neste capítulo, adotaremos a primeira opção. Para encontrar o maior valor da subárvore da esquerda, basta encontrar o valor que está mais à direita nessa subárvore. Veja, abaixo, o algoritmo que realiza a exclusão de um nodo em uma ár- vore. Foram definidas algumas funções auxiliares, como a função pesquisa, que pesquisa se um valor está ou não na árvore, retornando seu nodo (ela retorna NULL se o nodo não estiver na árvore). Também definimos a função busca _ pai, que retorna o pai do nodo que contém determinado valor. Por fim, a função max é utilizada para buscar o valor máximo da subárvore da esquerda quando ocorre uma remoção de nodo com dois filhos. função remove(Nodo raiz, int valor){ Nodo del = pesquisa(raiz, valor) se (del == NULL){ escreva(“O valor procurado não está na árvore.”) } senao se (del.filhoEsquerdo == NULL e del.filhoDi- reito == NULL){ // Remoção de um nodo folha Nodo pai = busca _ pai(valor) se (pai.FilhoEsquerdo.chave == valor) pai.filhoEsquerdo = NULL senao pai.filhoDireito = NULL } senao se (del.FilhoEsquerdo != NULL e del.filhoDi- reito != NULL){ // Remoção de Nodo de grau 2 Nodo pai = busca _ pai(valor) Árvores binárias em Java 9 Nodo maior = max(del.filhoEsquerdo) Nodo pai _ maior = busca _ pai(maior.chave) // Transfere os dados do maior para o del del.chave = maior.chave // Apaga o nodo maior se (maior.filhoEsquerdo == NULL e maior.filhoDireito == NULL){ se (pai _ maior.FilhoEsquerdo.chave == maior){ pai _ maior.FilhoEsquerdo = NULL } senao { Pai _ maior.FilhoDireito = NULL } } senao { // Maior possui um filho a esquerda se (pai _ maior.FilhoEsquerdo.chave == maior){ pai _ maior.FilhoEsquerdo = maior. FilhoEsquerdo } senao { Pai _ maior.FilhoDireito = maior.FilhoEsquerdo } } } senao { // Remoção de nodo de grau 1 Árvores binárias em Java10 Nodo pai = busca _ pai(valor) se (pai.FilhoEsquerdo.chave == valor) { se (del.filhoEsquerdo != NULL) pai.filhoEsquerdo = del.filhoEsquerdo senao pai.filhoEsquerdo = del.filhoDireito } senao { se (del.filhoEsquerdo != NULL) pai.filhoDireito = del.filhoEsquerdo senao pai.filhoDireito = del.filhoDireito } } } Vamos exemplificar cada caso de remoção com um exemplo prático. Considere a árvore obtida na Figura 4. Vamos remover os nodos 7, 8 e 3 da árvore, nessa ordem. O nodo 7 possui grau zero; logo, para que seja removido da árvore, basta apagar a referência de seu pai (nodo 6) para ele, resultando na árvore da Figura 6a. A remoção do nodo 8 se encaixa no segundo caso, visto que esse nodo possui uma subárvore. Nesse caso, devemos modificar a referência de seu pai (nodo 5) para o nodo 6, que é o filhodo nodo 8. Esse processo resultará na árvore da Figura 6b. Por fim, removendo o nodo 3, que possui dois filhos, demanda a escolha do maior nodo da subárvore da esquerda para substituí-lo. Como, nesse exemplo, só existe o nodo 2 na subárvore, ele substituirá o nodo 3, produzindo a árvore da Figura 6c. Árvores binárias em Java 11 Figura 6. Remoção de nodos 7 (a), 8 (b) e 3 (c) de uma árvore binária. Implementando uma árvore binária em Java Após estudarmos a terminologia básica e conhecer os principais métodos presentes em uma árvore binária, podemos implementar essa estrutura de dados na linguagem de programação Java, utilizando orientação a objetos. Aqui, vamos criar uma árvore que armazena somente um número inteiro, que também será a chave de busca na árvore. Iniciaremos definindo uma classe que representa um nodo da árvore, chamada de NodoArvore, conforme mostrado na Figura 3. public class NodoArvore { private NodoArvore filhoEsquerda; private int chave; private NodoArvore filhoDireita; public NodoArvore(int valor){ Árvores binárias em Java12 this.setChave(valor); this.setFilhoEsquerda(null); this.setFilhoDireita(null); } } Considere que essa classe também contém os getters e setters correspon- dentes aos três campos presentes na classe. Ao criar um nodo na árvore, é necessário informar o valor inteiro que será armazenado naquele nodo. Por padrão, os filhos esquerdo e direito daquele nodo são nulos. Vamos definir, agora, uma interface que contém os métodos públicos que devem estar presentes na estrutura de dados árvore: public interface Arvore { public NodoArvore pesquisa (int valor); public void inserir (int valor); public void remover (int valor); public void imprime _ preFixado(); } Essa interface possui quatro métodos: um método de consulta à árvore, chamado de pesquisa, os métodos de inserção e remoção, e, por fim, um mé- todo que imprime os valores presentes na árvore utilizando o caminhamento prefixado. Vamos, agora, definir a classe ArvoreBinaria, que implementa a interface Arvore definida anteriormente e contém uma instância de No- doArvore, que será a raiz da árvore. Esse objeto é inicializado com o valor NULL no construtor da classe. public class ArvoreBinaria implements Arvore { private NodoArvore raiz; public ArvoreBinaria(){ this.raiz = null; } // Os métodos da interface Arvore deverão ser inseri- dos aqui. } Árvores binárias em Java 13 Vejamos, agora, a implementação de cada um dos métodos da interface Arvore, bem como de alguns métodos auxiliares que permanecerão com a visibilidade privada dentro da classe ArvoreBinaria para que não sejam utilizados fora dessa classe. Método pesquisa na árvore binária O método de busca recebe, como parâmetro, o valor que se quer encontrar na árvore. Ele utiliza um método auxiliar recursivo para percorrer a árvore e buscar o valor informado. O método pesquisaRecursivo compara o valor buscado com o valor da chave da raiz. Se eles forem iguais, o método retorna aquele nodo; caso contrário, é checado se o valor informado como parâmetro é maior ou menor do que a chave do nodo atual. Caso seja menor, sabemos que o valor buscado só poderá estar na subárvore da esquerda e, se for maior, na subárvore da direita. Conforme o resultado do método, é feita uma chamada recursiva, tendo como parâmetro a subárvore correspondente ao resultado do teste. Veja, abaixo, a implementação dos métodos descritos. public NodoArvore pesquisa(int valor){ NodoArvore nodoResultado = pesquisaRecursivo(raiz, valor); return nodoResultado; } private NodoArvore pesquisaRecursivo(NodoArvore raiz, int valor) { if(raiz != null) { if(valor == raiz.getChave()){ return raiz; } else if (valor < raiz.getChave()) { return pesquisaRecursivo(raiz.getFilhoEs- querda(), valor); } else { return pesquisaRecursivo(raiz.getFilhoDi- reita(), valor); } } return null; } Árvores binárias em Java14 Método de impressão da árvore A impressão da árvore é feita por meio de um caminhamento prefixado. O método imprime _ preFixado faz uma chamada ao método recursivo preFixado, informando, como parâmetro, a raiz da árvore. Esse método, por sua vez, realiza o caminhamento pela árvore. Se o nodo atual é nulo, nada é feito; caso contrário, ele imprime o valor presente na raiz e faz duas chamadas recursivas, primeiramente para a subárvore da esquerda e, depois, para a subárvore da direita. public void imprime _ preFixado(){ preFixado(this.raiz); } private void preFixado(NodoArvore raiz){ if( raiz == null ) return; System.out.print(raiz.getChave() + " "); preFixado(raiz.getFilhoEsquerda()); preFixado(raiz.getFilhoDireita()); } Inserção de nodos na árvore Agora vamos analisar o método de inserção em uma árvore binária. O método inserir recebe um número inteiro como parâmetro e o insere na árvore. O primeiro teste que é feito é se a árvore está vazia. Se for o caso, então esse valor passa a ser a nova raiz da árvore. Se já houver uma raiz, é invocado o método recursivo insere, que vai procurar o local onde o novo nodo deve ser inserido a partir da raiz, de acordo com o critério de ordenação da árvore. Se o valor for menor do que a raiz, ele deve ser inserido ao lado esquerdo da árvore; caso seja maior, ele será inserido no lado direito. Após decidir em qual das subárvores o nodo deverá ser inserido, é necessário verificar se já existe algum nodo alocado naquela subárvore. Caso não exista, criamos um nodo e o inserimos naquela subárvore. Se existir um nodo, fazemos uma chamada recursiva à função insere, considerando o nodo da subárvore correspondente como raiz. Árvores binárias em Java 15 public void inserir (int valor){ if (raiz == null){ raiz = new NodoArvore(valor); } else { insere(raiz, valor); } } private void insere (NodoArvore raiz, int valor){ if (valor < raiz.getChave()){ if (raiz.getFilhoEsquerda() == null){ NodoArvore novoNodo = new NodoArvore(valor); raiz.setFilhoEsquerda(novoNodo); } else { insere(raiz.getFilhoEsquerda(), valor); } } else if (valor > raiz.getChave()){ if (raiz.getFilhoDireita() == null){ NodoArvore novoNodo = new NodoArvore(valor); raiz.setFilhoDireita(novoNodo); } else { insere(raiz.getFilhoDireita(), valor); } } } Remoção de nodo da árvore Por fim, vejamos como implementar a remoção de nodos de uma árvore. Como comentado na seção anterior, o método de remoção tem alguns casos especiais que precisam ser tratados. Estudaremos o código de cada um deles agora. Iniciaremos com o método remover, que decide qual dos casos deve ser tratado. Primeiramente, utilizaremos o método pesquisa para verificar se o valor informado como parâmetro da função realmente se encontra na árvore. Se ele não estiver na árvore, apenas informamos ao usuário, escre- vendo uma mensagem na tela. Se o valor for encontrado na árvore, verificamos quantos filhos ele possui e chamamos o método adequado: removeFolha, removeUmFilho ou removeDoisFilhos. Árvores binárias em Java16 public void remover (int valor){ NodoArvore nodo = pesquisa(valor); if (nodo == null){ System.out.println("O valor "+ valor +" não está na árvore."); } else if (nodo.getFilhoEsquerda() == null && nodo.getFilhoDireita() == null){ removeFolha(nodo); } else if (nodo.getFilhoEsquerda() != null && nodo.getFilhoDireita() != null){ removeDoisFilhos(nodo); } else { removeUmFilho(nodo); } } Vejamos, agora, como se dá a remoção do nodoem cada um dos três casos. Antes disso, vamos definir um método auxiliar chamado de buscaPai, que encontra o pai do nodo que deve ser removido. Esse método tem funciona- mento parecido com o método pesquisa; porém, no lugar de verificar se a raiz corresponde ao valor atual, ele checa se algum dos filhos possui o valor buscado. Esse método retorna null caso o nodo buscado não possua pai e, nesse caso, saberemos que se trata do nodo-raiz. private NodoArvore buscaPai(NodoArvore raiz, int valor) { if (raiz != null){ if (valor < raiz.getChave()){ if (raiz.getFilhoEsquerda() != null && raiz.getFilhoEsquerda().ge- tChave() == valor){ return raiz; } else { return buscaPai(raiz.getFilhoEsquerda(), valor); } } else if (valor > raiz.getChave()){ if (raiz.getFilhoDireita() != null && raiz.getFilhoDireita().getChave() == valor){ return raiz; } else { Árvores binárias em Java 17 return buscaPai(raiz.getFilhoDireita(), valor); } } } return null; } O método que remove um nodo folha é descrito abaixo. Usamos o método buscaPai para localizar o pai do nodo que será removido e, em seguida, fazemos um teste para decidir se o nodo a ser removido está à esquerda ou à direita de seu pai. Por fim, apagamos sua referência, setando o valor daquela subárvore para null. private void removeFolha(NodoArvore nodo){ NodoArvore pai = buscaPai(this.raiz, nodo.getChave()); if (pai == null){ this.raiz = null; } else if (pai.getFilhoEsquerda() != null && pai.getFilhoEsquerda() == nodo){ pai.setFilhoEsquerda(null); } else { pai.setFilhoDireita(null); } } Observe a comparação que fizemos na função removeFolha: pai. getFilhoEsquerda() == nodo. Nesse tipo de comparação, não estamos comparando os valores das chaves dos dois nodos, mas suas referências. Isso quer dizer que essa expressão retornará o valor true se pai. getFilhoEsquerda() e nodo forem o mesmo objeto. Caso sejam objetos diferentes, essa comparação retornará o valor false. Veja, agora, como fica a implementação do método que remove um nodo que possua um filho. Primeiramente, buscamos o pai do nodo que será remo- vido e, após isso, verificamos se estamos removendo a raiz da árvore. Se for verdadeiro, o filho passa a ser a nova raiz; caso contrário, encadeamos o nodo avô com seu neto, removendo o nodo informado como parâmetro da árvore. private void removeUmFilho(NodoArvore nodo){ NodoArvore pai = buscaPai(this.raiz, nodo.getChave()); if (pai == null){ Árvores binárias em Java18 if (nodo.getFilhoEsquerda() != null){ this.raiz = nodo.getFilhoEsquerda(); } else { this.raiz = nodo.getFilhoDireita(); } } else if (nodo.getFilhoEsquerda() != null) { if (pai.getFilhoEsquerda() == nodo){ pai.setFilhoEsquerda(nodo.getFilhoEsquerda()); } else { pai.setFilhoDireita(nodo.getFilhoEsquerda()); } } else if (nodo.getFilhoDireita() != null){ if (pai.getFilhoEsquerda() == nodo){ pai.setFilhoEsquerda(nodo.getFilhoDireita()); } else { pai.setFilhoDireita(nodo.getFilhoDireita()); } } } O método que exclui um nodo que possui dois filhos necessita de um método auxiliar max, que retorna o maior valor de uma árvore. Esse método será utilizado para encontrar o nodo que deverá substituir o nodo que será removido, conforme explicado na seção anterior. Seu funcionamento é bastante simples: ele percorre a subárvore da direita até encontrar um nodo que não possua um filho. Esse nodo contém o maior valor naquela árvore. private NodoArvore max(NodoArvore raiz){ if (raiz.getFilhoDireita() == null){ return raiz; } else { return max(raiz.getFilhoDireita()); } } Finalmente, vejamos o código correspondente ao método removeDois- Filhos. A princípio, esse método procura o pai do nodo a ser removido e o maior nodo de sua subárvore esquerda, que ocupará seu lugar. Além disso, buscamos o pai do maior nodo, para corrigirmos suas referências. Na sequência, modificamos o valor da chave do nodo que será removido Árvores binárias em Java 19 (e outros dados que também estejam armazenados naquele nodo) para o valor da chave (e outros dados) do nodo max e deletamos o maior nodo da árvore. Dessa forma, não precisaremos fazer muitas alterações nos apontadores dos nós. Por fim, procedemos com a remoção do nodo max da árvore. Será neces- sário fazer um teste para verificar se esse nodo possui algum descendente à esquerda (sabemos que, por ser o maior valor da árvore, ele não possui qualquer filho à direita). Caso ele não tenha, basta apagar sua referência na variável paiMax. Se ele possuir um filho, esse nodo passa a ocupar sua posição. private void removeDoisFilhos(NodoArvore nodo){ NodoArvore pai = buscaPai(this.raiz, nodo.getChave()); NodoArvore max = max(nodo.getFilhoEsquerda()); NodoArvore paiMax = buscaPai(nodo, max.getChave()); nodo.setChave(max.getChave()); if (max.getFilhoEsquerda() == null){ if (paiMax == nodo){ paiMax.setFilhoEsquerda(null); } else { paiMax.setFilhoDireita(null); } } else { paiMax.setFilhoDireita(max.getFilhoEsquerda()); } } Referências DEITEL, H. M.; DEITEL, P. J. JavaTM: como programar. 10. ed. São Paulo: Pearson, 2017. EDELWEISS, N.; GALANTE, R. Estruturas de dados. Porto Alegre: Bookman, 2008. 18 v. (Série Livros Didáticos informática UFRGS). FEOFILOFF, P. Algoritmos em linguagem C. Rio de Janeiro: Elsevier, 2008. FERRARI, R. et al. Estruturas de dados com jogos. Rio de Janeiro: Elsevier, 2014. GOODRICH, M. T.; TAMASSIA, R. Estrutura de dados e algoritmos com Java. 5. ed. Porto Alegre: Bookman, 2013. TANENBAUM, A. S.; WOODHULL, A. S. Sistemas operacionais: projeto e implementação. Porto Alegre: Bookman, 2008. Árvores binárias em Java20 Os links para sites da web fornecidos neste capítulo foram todos testados, e seu funcionamento foi comprovado no momento da publicação do material. No entanto, a rede é extremamente dinâmica; suas páginas estão constantemente mudando de local e conteúdo. Assim, os editores declaram não ter qualquer responsabilidade sobre qualidade, precisão ou integralidade das informações referidas em tais links. Árvores binárias em Java 21