Em Java, a maneira padrão de impor uma ordem entre objectos do mesmo tipo e permitir a comparação de dois objectos é usar o interface Comparable, que determina que a classe em questão deve implementar o método compareTo(). Se tivermos dois objectos o1 e o2 do mesmo tipo, então uma chamada a o1.compareTo(o2) deve devolver:
Um número negativo, se o1 for menor que o2
Zero, se o1 for igual a o2
Um número positivo, se o1 for maior que o2
Este exercício vai permitir-lhe explorar um pouco este interface.
Usando o método compareTo().
Compile e execute o seguinte programa, verificando o output.
Note como os wrappers padrão já implementam este interface! (uma listagem completa de todas as classes de Java que permitem o uso do compareTo pode ser vista aqui).
Métodos que usam o interface Comparable.
Alguns métodos do Java usam o interface Comparable para poder comparar objectos. Um exemplo é o Arrays.sort, que permite ordenar um array de objectos "comparáveis".
Para ver um exemplo, compile e execute o seguinte programa, verificando o output:
importjava.util.Arrays;// Testando o metodo Arrays.sort()classTestSort{publicstaticvoidmain(String[] args){// Inicializar array v
String[] v ={"ana","carlos","adalberto","maria","luis","filipa"};// Ordenar array v
Arrays.sort(v);// Se fosse escrito diretamente v, apenas seria mostrada a sua referência
System.out.println(Arrays.toString(v));}}
Uma classe customizada com comparador.
Compile e execute o seguinte pedaço de código, que tem como intuito imprimir uma tabela classificativa de equipas de futebol:
Que erros obtém? O java queixa-se que a classe Team não implementa o interface Comparable...
importjava.util.Arrays;// Classe para representar uma equipaclassTeam{
String name;int points;// Construtor
Team(String n,int p){
name = n;
points = p;}// Método para dar representação em String de um objecto da classepublic String toString(){return"("+ name +","+ points +")";}}// Testando o método Arrays.sort()classTestTeams{publicstaticvoidmain(String[] args){// Inicializar array v
Team[] v =new Team[7];
v[0]=new Team("Leicester",20);
v[1]=new Team("Chelsea",31);
v[2]=new Team("Arsenal",20);
v[3]=new Team("Liverpool",42);
v[4]=new Team("Man United",37);
v[5]=new Team("Man City",20);
v[6]=new Team("Tottenham",39);// Ordenar e escrever array v
Arrays.sort(v);
System.out.println(Arrays.toString(v));}}
Indicando que se pretende implementar o comparador.
Altere a linha class Team para class Team implements Comparable<Team> e recompile. Que erros de compilaçao obtém agora?
does not override abstract method compareTo(Team) indica que não foi dada uma implementação do método compareTo para a classe Aluno, apesar do interface Comparable assim o exigir.
Implementando um comparador.
Adicione o seguinte código dentro da classe Team: public int compareTo(Team t) {
return name.compareTo(t.name);
}
Recompile, execute e verifique o output.
Estamos a dizer que comparar duas equipas é equivalente a comparar os seus nomes.
Note como as equipas aparecem por ordem alfabética crescente
Corrigindo o comparador.
Corrija a implementação do comparador para que a ordenação venha por ordem decrescente de pontuação, e em caso de empate por ordem alfabética crescente. Recompile e execute para testar.
No método compareTo, acrescente linhas para primeiro comparar os pontos e devolver um número positivo ou negativo caso os pontos sejam menores ou maiores (cuidado com o sinal do número devolvido para que fique ordem decrescente)
Se o número de pontos não for maior ou menor... compara-se os nomes com a linha que já lá estava anteriormente!
Exercício 2) Compreender Árvores Binárias de Pesquisa
Código de árvores binárias de pesquisa.
Faça download do código dado nas teóricas e compile-o:
BSTree - Árvore binária de pesquisa (ex. métodos: número de nós, altura, contains, inserção e remoção; impressões preorder, inorder, postorder, em largura)
(note que também precisa do código de listas, pilhas e filas, por causa dos métodos de DFS e BFS)
Espreite o ficheiro BSTNode.java para ver como um nó é descrito, e note na assinatura como o tipo T tem de ser comparavél ou seja, implementa o interface Comparable.
Um primeiro teste.
Execute a classe de teste das árvores binárias de pesquisa: $ java TestBSTree
Espreite e acompanhe a implementação do método insert da classe BSTree. Relembre que numa árvore binária de pesquisa, para um qualquer nó, todos os elementos da subárvore esquerda são menores e todos os elementos da subárvore direita são maiores. Para o ajudar a compreender, a imagem seguinte mostra o conteúdo da árvore após cada inserção (em cada passo o elemento inserido está indicada a amarelo):
Depois da remoção de 14, que número ficou na raíz? Acompanhe a implementação do método remove da classe BSTree para verificar como isto é feito.
Visualizando Inserções e remoções.
Procure desenhar no seu caderno como ficaria uma árvore binária de pesquisa depois de inserir os seguintes elementos (por esta ordem): 4 8 1 3 9 2 7 6.
Depois de desenhar, use a seguinte página para verificar se acertou no desenho da árvore:
Experimente retirar e inserir mais alguns elementos, procurando primeiro confirmar como ficaria a árvore, e depois usando a página para confirmar o que previu.
Exercício 3) Alguns exercícios com árvores binárias de pesquisa
Para que um nó x esteja entre a e b, quais devem ser os resultados de x.compareTo(a) e x.compareTo(b)?
Use recursividade para procurar os elementos por toda a árvore
Procure aproveitar as características duma árvore binária de pesquisa: por vezes sabemos que todo um ramo não está contido no intervalo (ex: se x é menor que a, então todo a subárvore esquerda não pertence ao intervalo, pois são elementos menores que x)
Exercício 4) Usando árvores binárias de pesquisa para representar conjuntos
Uma árvore binária de pesquisa mapeia muito bem num TAD conjunto. Este exercício pede-lhe precisamente para resolver de forma simples dois pequenos problemas que na sua essência pode ser resolvidos pensando em conjuntos.
Este problema é uma versão simplificada de um problema resolvido nas aulas teóricas (ver slides 70 e 71)
Crie uma árvore binária de pesquisa para guardar strings
Insira as palavras na árvore de pesquisa (método insert()). Os elementos iguais são descartados. Para ler palavras pode usar o método next() da classe Scanner.
No final imprima o número de nós da árvore (método numberNodes())
Crie uma árvore binária de pesquisa para guardar inteiros
Depois de ler os números, passe por todas as somas possiveis (combinações dois a dois - basta fazer dois ciclos) e guarde-as na árvore (método insert()). Note que as somas iguais a anteriores são descartadas (a árvore guarda apenas uma cópia de cada número)
Para responder a cada pergunta basta verificar se o número em causa está ou não contido na árvore (método contains())
Exercícios extra para consolidação de conhecimentos
Para consolidar os seus conhecimentos do interface Comparable, resolva e submeta o seguinte problema (disponível no volume de árvores), que é uma extensão do exercício 1 desta aula:.
Acrescenta à classe Team atributos com o que precisa para ordenar e imprimir: além do número de pontos e nome da equipa, precisa do goal average
Crie um array de Teams para guardar os dados de leitura. Ao ler pode transformar logo o número de vitórias, empates e derrotas no número de pontos, e o número de golos marcados e sofridos no goal average.
Use o método Arrays.sort para ordernar. Recorde que para o usar a classe Team deve implementar Comparable<Team> e defina o método compareTo(Team t) para devolver um valor de acordo com o que o problema pede:
Comece por tentar ordenar apenas por pontos e verifique se o seu programa está a funcionar
Adicione depois a camada de, em caso de empate por pontos, verificar o goal average
Finalmente adicione o critério final de desempate pelo nome.
Para garantir que percebe as condições necessárioas para uma árvore binária ser de pesquisa, resolva e submeta o seguinte problema:
Uma árvore é de pesquisa se ambas as suas subárvores também o são.
Em cada nó, deve verificar se este é válido, ou seja:
Se todos os nós da subárvore esquerda são menores que o nó atual
Se todos os nós da subárvore direita são maiores que o nó atual
Cuidado! Não pode usar directamente os métodos anteriores de máximo e mínimo, pois se a árvore não for binária de pesquisa não é garantido que o mínimo e máximo estejam nessas posições.
Para consolidar os seus conhecimentos sobre a árvores binárias de pesquisa, sugerimos que explore a altura de uma árvore binária de pesquisa com dados aleatórios. O seguinte pedaço de código gera 20 árvores binárias de pesquisa com 100 000 valores aleatórios entre 0 e 109-1. Antes de executar, procure ter uma ideia da ordem de grandeza da altura que vai encontrar. Depois de estimar, compile e execute para verificar se acertou na magnitude do valor.
Experimente mudar a quantidade de valores para 10x menos (104) ou 10x mais (106) e veja a altura média das árvores geradas.
Note como o tamanho das árvores cresce apenas logaritmicamente com o número de valores (ou seja, se n for o número de valores, a altura é O(log n).
Note também como O(log n) é MUITO melhor que O(n) (quantas comparações é necessário fazer numa árvore destas em comparação com um array onde tenha de passar por todos os elementos?).
Experimente variar entre múltiplos de 10 (entre 101 e 107) e tomar nota da altura média, tentando depois arranjar uma constante c tal que c*log2n seja sempre igual ou superior à altura média (validando experimentalmente a afirmação de que a altura é O(log n)).
importjava.util.Random;// Classe para gerar várias árvores de pesquisa com elementos aleatóriosclassTestRandom{// Em Java, constantes declaram-se com classificador "final"finalstaticint NUM_TREES =20;// Número de árvores de pesquisafinalstaticint NUM_VALUES =100000;// Quantidade de valores por árvorefinalstaticint MAX_VALUE =1000000000;// Máximo valor num nópublicstaticvoidmain(String[] args){// variáveis para guardar altura total, mínima e máximaint total=0, min=0, max=0;// Objecto para gerar números aleatórios
Random r =new Random();// Ciclo para iterar sobre cada uma das árvores binárias de pesquisafor(int i=1; i<=NUM_TREES; i++){// Criar uma árvore (inicialmente vazia)
BSTree<Integer> t =new BSTree<Integer>();// Ciclo para iterar sobre cada um dos valores a inserirfor(int j=0; j<NUM_VALUES; j++){int x;// Se valor já existe na árvore, gerar novamentedo{// Gerar um valor aleatório entre 0 e MAX_VALUE-1
x = r.nextInt(MAX_VALUE);}while(t.contains(x));// Inserir valor na árvore
t.insert(x);}// Actualizar variáveis com dados sobre alturaint h = t.depth();
total += h;if(i==1|| h<min) min = h;if(i==1|| h>max) max = h;
System.out.printf("Arvore #%2d: altura = %d | num. nos = %d%n",
i, h, t.numberNodes());}// Escrever estatísticas gerais
System.out.printf("Altura média: %.2f%n",(double)total/NUM_TREES);
System.out.println("Altura mínima: "+ min);
System.out.println("Altura máxima: "+ max);}}
Exercício de Desafio
Para esta semana vou colocar um exercício que envolve árvores mas também eficiência algorítmica. É um problema da minha autoria que que foi usado nas Olimpíadas de Informática:
O limite de tempo de execução para cada caso de teste é de 1 segundo, pelo que a solução só será aceite e com pontuação máxima no Mooshak se for eficiente. Não será por isso possível para cada pergunta simular realmente todas as bolas a cair até chegar n-ésima bola.
Para estes problemas de desafio não vou dar logo dicas, para vos deixar pensar, mas se quiserem mesmo resolver o problema e não estejam a conseguir (mesmo depois de terem realmente tentado), podem falar comigo para obter pistas, ou ter uma ideia de como os "atacar".