Mostrando postagens com marcador Estrutura de dados. Mostrar todas as postagens
Mostrando postagens com marcador Estrutura de dados. Mostrar todas as postagens

quarta-feira, 17 de maio de 2017

Indicações de livros - Estruturas de dados e Algoritmos em Java

Bem-vindos ao blog Preciso Estudar Sempre. Meu nome é João Paulo Maida e minha paixão é estudar.

Para dar aquela quebrada na rotina, neste mês trago a indicação de um excelente livro através de um vídeo do nosso canal do YouTube (links lá embaixo). Não perca, ficou muito bom. :D

Livro: Estruturas De Dados E Algoritmos em Java
Autor: Robert Lafore
Editora: CIENCIA MODERNA
Sumário:
  • Capítulo 1 - Visão geral
  • Capítulo 2 - Vetores (se você já sabe, pode pular)
  • Capítulo 3 - Ordenação simples (recomendo muito)
  • Capítulo 4 - Pilhas e filas (importante para qualquer desenvolvedor)
  • Capítulo 5 - Listas encadeadas (importante para qualquer desenvolvedor)
  • Capítulo 6 - Recursão (diferencia homens de meninos)
  • Capítulo 7 - Ordenação avançada (recomendo muito)
  • Capítulo 8 - Árvores binárias (importante para qualquer desenvolvedor)
  • Capítulo 9 - Árvores rubro-negras (inside the matrix)
  • Capítulo 10 - Árvores 2-3-4 e armazenamento externo (inside the matrix)
  • Capítulo 11 - Tabelas Hash (importante para qualquer desenvolvedor)
  • Capítulo 12 - Heaps (inside the matrix)
  • Capítulo 13 - Grafos (se você gosta de assuntos mais complicados, aqui é o lugar)
  • Capítulo 14 - Grafos ponderados (se você gosta de assuntos mais complicados, aqui é o lugar)
  • Capítulo 15 - Quando usar o que ? (um dos melhores capítulos)

Até a próxima minha boa gente ! 😘

Leia nossa postagem anterior: O algoritmo Warshall - Grafos

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários, envie um e-mail ou uma mensagem na nossa página do Facebook.

E-mail: precisoestudarsempre@gmail.com
Facebook: https://www.facebook.com/precisoestudarsempre/
Canal Preciso Estudar Sempre: https://www.youtube.com/channel/UCUoW8dS38rXr0a5jWU57etA
Leia Mais ››

terça-feira, 27 de dezembro de 2016

Questões de Concurso - Prova 304 - DataPrev Analista de Negócios 2016

Bem-vindos ao blog Preciso Estudar Sempre. Meu nome é João Paulo Maida e minha paixão é estudar.

Se você chegou aqui agora e não está entendendo sobre o que é essa postagem, recomendo que você dê uma lida primeiro neste post aqui, e depois volte para continuar lendo. Para você que já sabe do que estou falando, vamos começar.

A prova que vamos analisar é a 304 - DataPrev Analista de Negócios 2016. Consegui selecionar três questões para debatermos, são elas:

Questão 43 - Fácil 😝

Questão 43 da prova 304 DataPrev Analista de Negócios
Figura 1 - Questão 43 da prova 304 DataPrev Analista de Negócios


Pare sua leitura por aqui, tome o seu tempo, leia calmamente a questão e tente encontrar a resposta. Conseguiu achar ?

Questão 43 - Resposta
Questão 43 respondida da prova 304 DataPrev Analista de Negócios
Figura 2 - Questão 43 respondida da prova 304 DataPrev Analista de Negócios
Entendamos a questão ! Se concentre nela !

A resposta correta é a opção E. Mas porque esta opção e não a opção D, por exemplo ? A questão fala sobre um dos benefícios da programação orientada a objetos, a abstração de classes e objetos. Isto é importante, pois ele será o delimitador para que não escolhamos a opção errada. Consequentemente a opção D se desqualifica, pois ela engloba a afirmação 3, onde esta cita a utilização de vários padrões conceituais durante todo o processo de criação de software e isto não está relacionado diretamente com a abstração de classes e objetos. Este momento é muito inicial no desenvolvimento de um sistema e esta afirmação cita de algo muito mais à frente.

A opção A também se desqualifica, pois engloba a afirmação 3, e a opção B também está errada,  mesmo não incluindo a afirmação 3, mas deixando de incluir uma afirmação que está correta, a 2. Esta está correta pois de fato a orientação a objetos traz essa característica. A possibilidade de modelar dados através de classes e objetos eleva o programador a um outro nível de desenvolvimento de software, permitindo que ele desenvolva um código mais limpo e fácil para manutenção e reuso.

A opção C deixa de lado a afirmação 1 o que a torna também errada. Através da orientação a objetos é possível criar estruturas de classes e interfaces conectadas entre si que tornem a reutilização de código maior. Polimorfismo e herança são ótimos exemplos deste caso.

Questão 44 - Médio 😑
Questão 44 da prova 304 DataPrev Analista de Negócios
Figura 3 - Questão 44 da prova 304 DataPrev Analista de Negócios
Mais uma vez tome o seu tempo e leia cuidadosamente a questão. Essa aí está moleza!

Sobre o que o enunciado se refere ? Simples ! Ele narra a situação onde existe uma classe abstrata e todas as classes que a estendem, se tornando assim filhas, são concretas.
Diagrama de classes de exemplo
Figura 4 - Diagrama de classes de exemplo
A Figura 4 exemplifica a situação que a questão narra. A classe Montadora é abstrata, logo ela não pode sofrer instanciação direta através do operador new. Então seus filhos entram em ação sendo representações concretas de um pai abstrato, onde em cada um é definido um comportamento particular, e eles sim podem ser instanciados. Tal comportamento é exemplificado pelo método montar().

Imagine o seguinte cenário: surgiu a necessidade de se trabalhar com dados reais, ou seja, centenas de montadoras, onde cada uma monta seus carros de formas completamente diferentes. Como representar isso? Uma única classe Montadora com todos as formas possíveis de se montar um carro não seria uma boa escolha, pois vc acabaria com uma classe com alguns milhares de linhas. Talvez modificações em uma parte do código poderiam criar bugs em outras, afetando assim a linha de montagem de outros automóveis.

Da forma mostrada pela Figura 4 é possível especializar a montagem de carros em classes diferentes e adicionar novas montadores sem mexer nas que já existem. Mas porque especializar ? Especializar é importante pois assim cada uma conhece sua forma de montar carros e não precisa conhecer o método empregado pelas outras ou de características gerais a todas montadoras. Tudo o que for comum a todas as montadoras pode ser posto na classe abstrata, e assim a classe especialista (concreta) se preocupa unicamente com sua responsabilidade no momento, montar carros.

Questão 44 - Resposta
Questão 44 respondida da prova 304 DataPrev Analista de Negócios
Figura 5 - Questão 44 respondida da prova 304 DataPrev Analista de Negócios
A questão pergunta qual é o conceito obtido quando se obtém novos exemplares a partir de uma representação abstrata. Este processo já foi citado nesta explicação, é a instanciação. Logo, a resposta correta é a opção B.

Questão 45 - Fácil 😝
Questão 45 da prova 304 DataPrev Analista de Negócios
Figura 6 - Questão 45 da prova 304 DataPrev Analista de Negócios
Respire fundo e tome o seu tempo.

Para responder esta pergunta precisaremos relembrar um conceito inicial da orientação a objetos, o encapsulamento. Muitas vezes é necessário restringir a visibilidade de características (atributos) e comportamentos (métodos) de uma classe, para que recursos externos não utilizem eles de forma indevida. A falta dessa atividade pode tornar o seu projeto de software fraco contra ataques de softwares maliciosos, pois estes podem ocasionar erros propositalmente levando assim a um travamento total do software. Segundo CHESS, BRIAN; WEST, JACOB o que torna o seu software fraco não são problemas de ambiente, como antivírus, firewall, etc.; mas sim erros arquiteturais como este.

Um ótimo exemplo desta situação é uma classe de acesso a banco de dados. Seus únicos comportamentos são: conectar e desconectar; e seus únicos atributos são: login, senha e o driver do banco. Reflita sobre a seguinte questão: qual seria a necessidade de tornar o login e senha do banco de dados visível para qualquer classe do sistema ? A resposta é simples: nenhuma. Neste momento o encapsulamento entra em ação.

É possível criar métodos que encapsulem estas informações. Para o atributo login, por exemplo, não é interessante fornecer uma forma que uma classe usuária possa obter qual login sendo utilizado no momento, mas talvez seria bom fornecer uma forma de se atribuir um login novo caso o default não funcione. Com o encapsulamento os métodos de acesso se tornam os únicos responsáveis pelos recursos que manipulam, sendo eles atributos ou métodos, formando assim um canal de acesso único.

Espero que com a explicação dada a resposta da questão se torne clara.

Questão 45 - Resposta
Questão 45 respondida da prova 304 DataPrev Analista de Negócios
Figura 7 - Questão 45 respondida da prova 304 DataPrev Analista de Negócios
As opções A, B, D e E são totalmente relacionadas ao encapsulamento, como já citado acima. A única que não é relacionada é a opção C, sendo ela a resposta final.

Com isso terminamos as questões da prova 304 - DataPrev Analista de Negócios. Espero que vocês tenham gostado desse novo formato, e se quiser sugiram provas e questões. 😁

Até a próxima ! 😘

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários, envie um e-mail ou, deixa uma mensagem na nossa página do Facebook.

E-mail: precisoestudarsempre@gmail.com
Canal Preciso Estudar Sempre: https://www.youtube.com/channel/UCUoW8dS38rXr0a5jWU57etA

Referências

CHESS, BRIAN; WEST, JACOB; Secure Programming with Static Analysis; 2007
Leia Mais ››

Questões de Concurso - O início

Bem-vindos ao blog Preciso Estudar Sempre. Meu nome é João Paulo Maida e minha paixão é estudar.

Este é o primeiro post de uma nova série de postagens aqui no blog. Sempre que você ver um post com o título começando com Questões de Concurso, já pode esperar algumas belas questões, suas respostas e explicações.

Mas que tipo de questão você vai trazer João ? Sempre trarei questões relacionados à programação, orientação a objetos, estruturas de dados e algoritmos. Todos esses assuntos já estamos discutindo aqui a um certo tempo, logo você tem um grande repositório de material para tirar suas dúvidas. 😊

Minha motivação para começar essa nova série foi trazer uma nova forma de conteúdo pro blog e agregar mais para a comunidade que é ou está focada em concursos públicos.

Este post, especificamente, será o catálogo de todas as postagens dessa nova série. Caso você queira fazer uma pesquisa em todas as provas já comentadas, pode dar uma olhada aqui.

Posts da série Questões de Concurso:

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários, me mande um e-mail ou, na nossa página do facebook.

E-mail: precisoestudarsempre@gmail.com
Leia Mais ››

segunda-feira, 4 de abril de 2016

Desvendando as pilhas

Bem-vindos ao blog Preciso Estudar Sempre. Meu nome é João Paulo Maida e minha paixão é estudar.

Começamos esse post com a seguinte pergunta: como manter dados organizados da mesma forma que um vetor mas, acessando (inserindo e removendo) seus elementos somente pelo início ou fim dele ?

Você poderia responder que realizar essa tarefa é possível através do controle do índice do vetor visto que, os vetores oferecem acesso direto aos seus elementos. Contudo, isso seria algo muito complicado porque a cada inserção ou remoção, todos os elementos existentes teriam de ser deslocados para a esquerda, no caso da inserção, ou para a direita, no caso da remoção.

A idéia é: não queremos esse trabalho, queremos facilidade.

Para respondermos essa pergunta, é necessário ir além, entender um novo conceito. Precisamos entender  as pilhas ou, em inglês, stacks. É importante deixar claro que não estou falando das pilhas da Figura 1.
Figura 1 - Pilhas Duracell
Uma pilha, na ciência da computação, é uma estrutura de armazenamento de dados em que seu acesso só pode ser feito através de um topo. Imagine a seguinte situação: sua mãe te obrigou a lavar todos os pratos que estavam na pia. Depois de limpos, você começou a empilhá-los. Sua mãe viu você fazendo isso, te deu aquele belo esporro e mandou você secá-los. Quando você ia começar a secar, ela te deu um outro esporro, pois você tentou pegar um prato que estava no meio do pilha. Fazer isso, pode causar a quebra de todos os pratos que estão acima do que você tinha escolhido.

E aí, você apanhou.
Figura 2 - Sua mãe furiosa
É amigo ou amiga, a vida não é fácil. Mas então, como você seca os pratos ?

A resposta é simples. Para manter a integridade da sua pilha de pratos, você deve acessar somente o topo dela e assim, pegar um prato de cada vez, secá-lo e por numa nova pilha de pratos secos. Assim, você não apanha.
Figura 3 - Sua mãe feliz
Assim como é necessário manter a integridade da pilha de pratos, também é necessário manter a integridade da nossa pilha de dados. Logo, as mesmas regras devem ser seguidas. Contudo, uma pergunta ainda permanece: porque usar pilhas ?

Com elas é possível modelar soluções de uma forma simples e clara. Soluções, as quais se fossem modeladas com o uso de vetores resultariam em mais trabalho.

As pilhas são entidades mais abstratas que vetores e muitas outras estruturas de armazenamento de dados. Elas são definidas principalmente por sua interface, ou seja, as operações permitidas que podem ser executadas nelas. O mecanismo subjacente usado para implementá-las não é visível para o usuário.

Um problema que pode ser resolvido com o uso de pilhas, é o problema da palavra inversa. Esse problema consiste que dada uma determinada palavra, o resultado desejado é ela ao inverso, ou seja, para a palavra "casa", o retorno esperado é "asac".

Como resolver isso ?

Para que possamos começar, precisamos entender as duas operações básicas de uma pilha: push e pop.

Push significa empurrar logo, essa operação consiste em adicionar um item pelo topo na pilha. Pop significa remover um item pelo topo da pilha. Simples não !?
Figura 4 - As operações de uma pilha
Então, nosso problema da string reversa já pode ser resolvido. O que precisamos fazer é recuperar uma string digitada pelo usuário, empilhar letra por letra e depois, desempilhar completamente a pilha. Com isso, atingimos uma string reversa para qualquer uma fornecida pela usuário.

Já temos tudo o que precisamos para criar nossa implementação de pilha.

A classe Stack

1:  class Stack {  
2:         
3:       private char[] items;  
4:       private int counter;  
5:       private int size;  
6:    
7:       public Stack(int size){  
8:            items = new char[size];  
9:            this.size = size;  
10:            counter = 0;  
11:       }  
12:    
13:       public void push(char item) throws FullStackException{  
14:            if(isFull()){  
15:                 throw new FullStackException("Can't push. Stack is full.");  
16:            }  
17:            items[counter] = item;  
18:            counter++;  
19:       }  
20:    
21:       public char pop() throws EmptyStackException{  
22:            if(isEmpty()){  
23:                 throw new EmptyStackException("Can't remove. Stack is empty.");  
24:            }       
25:            counter--;  
26:            char item = items[counter];            
27:            return item;  
28:       }  
29:    
30:       public boolean isEmpty(){  
31:            return counter == 0;  
32:       }  
33:    
34:       public boolean isFull(){  
35:            return counter == size;  
36:       }  
37:    
38:       public char peek() throws EmptyStackException{  
39:            if(isEmpty()){  
40:                 throw new EmptyStackException("Can't remove. Stack is empty.");  
41:            }  
42:            return items[counter];  
43:       }  
44:  }  

Nossa pilha trata dados do tipo char visto que, precisamos tratar letra por letra. Os métodos push e pop fazem exatamente o que discutimos acima. Contudo, é importante ressaltar que na nossa implementação de método pop, o item removido é retornado para o usuário.

Os métodos isEmpty() e isFull() avaliam, respectivamente, se a pilha está vazia ou cheia. Essas verificações são feitas através do contador interno da pilha, a variável counter. Se esta variável for igual 0, a pilha está vazia e se for igual ao tamanho da pilha, o qual foi definido no construtor da classe, a pilha está cheia. O método peek() retorna o valor que está no topo da pilha, sem removê-lo.

Foram utilizadas exceções para todos os tratamentos de erro da pilha. Caso a pilha esteja vazia, em qualquer situação de acesso, ou seja, no uso do método pop e do método peek, uma EmptyStackException é propagada para informar ao usuário que não será possível completar a operação pois, a pilha está vazia. Uma FullStackException é propagada pelo método push, caso a pilha esteja cheia.

A classe ReverseString

1:  import java.util.Scanner;  
2:    
3:  class ReverseString {  
4:       public static void main(String[] args) {  
5:            ReverseString reverseString = new ReverseString();            
6:            String sourceString = reverseString.getSourceString();  
7:            char[] charArray = sourceString.toCharArray();  
8:            Stack stack = new Stack(sourceString.length());  
9:    
10:            for(int i=0;i<charArray.length;i++){  
11:                 try{  
12:                      stack.push(charArray[i]);  
13:                 } catch (FullStackException e) {  
14:                      e.printStackTrace();  
15:                      System.exit(1);  
16:                 }  
17:            }  
18:    
19:            System.out.print("String reversa: ");  
20:    
21:            while(!stack.isEmpty()){  
22:                 try{                      
23:                      System.out.print(stack.pop());  
24:                 } catch (EmptyStackException e) {  
25:                      e.printStackTrace();  
26:                      System.exit(1);  
27:                 }  
28:            }  
29:       }  
30:    
31:       public String getSourceString(){  
32:            Scanner input = new Scanner(System.in);  
33:            System.out.print("Informe a string: ");  
34:            return input.nextLine();  
35:       }  
36:  }  

Essa é a nossa classe cliente que utiliza nossa implementação de pilha. O método getSourceString() recupera o que foi digitado no console. No método main, utilizamos a string recuperada e realizamos a inversão, através dos métodos push e pop da pilha.

Vamos ver funcionando ?

Compile todos os arquivos .java.

Figura 5 - Compilação dos arquivos fonte
Execute e veja o resultado !!!

Figura 6 - Execução do programa
Terminamos !!

Link no dropbox:
Link no github: https://github.com/PrecisoEstudarSempre/Stacks.git

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários, me mande um e-mail ou, na nossa página do facebook.

E-mail: precisoestudarsempre@gmail.com
Facebook: https://www.facebook.com/precisoestudarsempre/

Referências bibliográficas:
Estruturas de dados e algoritmos em Java - Lafore, Robert

Imagens:
Figura 2 - http://noticiasdatv.uol.com.br/media/_versions/tichina_arnold_free_big.jpg
Figura 3 - http://www.clickcultural.com.br/foto/45/per_ft1_145.jpg
Figura 4 - http://botbench.com/blog/wp-content/uploads/2013/01/image5.png
Leia Mais ››

domingo, 29 de novembro de 2015

Dividindo e conquistando - O algoritmo Merge Sort

Bem-vindos ao blog Preciso Estudar Sempre. Meu nome é João Paulo Maida e minha paixão é estudar. Hoje abordaremos mais um tópico da série: Vamos por ordem nessa bagunça ? - Algoritmos de ordenação

Clique para dar uma olhada.

Neste post aprenderemos o algoritmo de ordenação Merge Sort.

AVISO: Esse algoritmo não é para iniciantes logo, você vai precisar de algum tempo para entendê-lo. Procure a supervisão de um adulto. =)

A nossa primeira pergunta é: como ele funciona ?

Criado por John von Neumann em 1945, este algoritmo utiliza a técnica de dividir para conquistar, diferentemente do seu primo, o algoritmo Bubble Sort. Possui complexidade de tempo Θ(n log 2n), no pior caso e no melhor caso, Θ(n log n).
Figura 1 - Criador do algoritmo Merge Sort
O algoritmo Merge Sort é de fácil implementação. É conceitualmente mais fácil do que a ordenação quicksort e a ordenação Shell mas, possui uma desvantagem. Requer um vetor adicional na memória, igual em tamanho àquele sendo ordenado. Se seu vetor original mal couber na memória, a ordenação não funcionará. Porém, se você possuir bastante espaço, isso não será um problema.

Sua estratégia consiste em criar uma sequência ordenada a partir de outras duas já ordenadas. Para tal, divide-se a sequência original em pares de dados, e ordena-se. Depois, agrupa-se em sequências de quatro elementos, e assim por diante até a sequência original estar separada em apenas duas partes. Este processo se repete até o array estar totalmente unido e ordenado.

A imagem abaixo ilustra bem o explicado.

Figura 2 - Funcionamento do algoritmo Merge Sort
Interprete cada faixa da imagem como uma etapa, ou seja, na primeira etapa é quando o array está todo unido, e assim por diante. É possível notar claramente como esse algoritmo funciona. Na primeira etapa temos o array todo unido, na segunda já dividimos ele ao meio (a divisão não é exata porque o tamanho do array não é par), ou seja, já estamos utilizando da técnica dividir para conquistar e esse processo de divisão se repete até a etapa 4.

Na etapa 4 dividimos o array até o máximo, ou seja, quando sobra somente 1 elemento, esse é o nosso caso base. A partir daí, podemos começar a ordená-lo. Na etapa 5 realizamos a primeira ordenação, ou seja, a ordenação dos pares. A partir da ordenação dos pares, podemos unir eles e ordená-los novamente, Esse processo se repete até o ponto em que obtemos o array todo unido novamente só que, agora ordenado.

O gif abaixo mostra todo o processo de ordenação de forma animada.
Figura 3 - Animação de funcionamento do algoritmo Merge Sort
Mas como transformar toda essa inteligência em um programa Java ?

1:  public class MergeSort {  
2:       private int[] numbers;  
3:       private int[] helper;  
4:    
5:       private int number;  
6:    
7:       public void sort(int[] values) {  
8:            this.numbers = values;  
9:            number = values.length;  
10:            this.helper = new int[number];  
11:            mergeSort(0, number - 1);  
12:       }  
13:    
14:       private void mergeSort(int begin, int end) {  
15:            // check if begin is smaller then end, if not then the array is sorted  
16:            if (begin < end) {  
17:                 // Get the index of the element which is in the middle  
18:                 int middle = begin + (end - begin) / 2;  
19:                 // Sort the left side of the array  
20:                 mergeSort(begin, middle);  
21:                 // Sort the right side of the array  
22:                 mergeSort(middle + 1, end);  
23:                 // Combine them both  
24:                 merge(begin, middle, end);  
25:            }  
26:       }  
27:    
28:       private void merge(int begin, int middle, int end) {  
29:    
30:            // Copy both parts into the helper array  
31:            for (int i = begin; i <= end; i++) {  
32:                 helper[i] = numbers[i];  
33:            }  
34:    
35:            int i = begin;  
36:            int j = middle + 1;  
37:            int k = begin;  
38:            // Copy the smallest values from either the left or the right side back  
39:            // to the original array  
40:            while (i <= middle && j <= end) {  
41:                 if (helper[i] <= helper[j]) {  
42:                      numbers[k] = helper[i];  
43:                      i++;  
44:                 } else {  
45:                      numbers[k] = helper[j];  
46:                      j++;  
47:                 }  
48:                 k++;  
49:            }  
50:            // Copy the rest of the left side of the array into the target array  
51:            while (i <= middle) {  
52:                 numbers[k] = helper[i];  
53:                 k++;  
54:                 i++;  
55:            }  
56:       }  
57:  }  

Pronto, conseguimos !! Vamos entender agora como o programa Java implementa o algoritmo.

Classe MergeSort

Essa classe possui os métodos de ordenação e os atributos numbers, helper e number os quais representam respectivamente, o array de valores, o array auxiliar e a variável usada para armazenar o tamanho do array de valores.

Esta implementação já possui uma otimização implementada para o problema de memória citado acima. Da forma que foi projetado, o array auxiliar só é carregado uma vez em memória visando aliviar o consumo.

Método sort

Esse método recebe o array original e realiza as seguintes operações:

  • Linha 8: passa os valores do array, recebido como parâmetro, para o array atributo, numbers.
  • Linha 9: Define o valor do atributo number onde, esse valor é o tamanho do array original.
  • Linha 10: Define a dimensão do array auxiliar onde, essa dimensão é a mesma do array original.
  • Linha 11: Realiza a chamada ao método mergeSort, passando como parâmetros o número 0 e o resultado da operação number - 1 onde, zero representa o início de qualquer array e number - 1 representa a última posição do array.


Método mergeSort

Esse método realiza a divisão já citada acima. Recebe dois parâmetros: a posição inicial e final do array.

  • Linha 16: Verifico se o parâmetro begin é menor que o parâmetro end. Essa verificação é necessária pois, ela realiza o controle da divisão do array. Caso a condição não seja verdadeira é porque o array está vazio ou, já está ordenado.
  • Linha 18: Calculo o meio do array visando a divisão do mesmo.
  • Linha 20: Realizo chamada recursiva passando como parâmetros begin e middle, ou seja, estou analisando a primeira metade do array dividido. Como esta é uma chamada recursiva, as linhas anteriores (16 e 18) serão executadas novamente, até o momento em que reste somente uma posição no array dividido n-vezes.
  • Linha 22: Realizo chamada recursiva passando como parâmetros middle +1 end, ou seja, estou analisando a segunda metade do array dividido. O processo de divisão segue de forma igual ao citado acima.
  • Linha 24: Realizo chamada ao método merge passando os parâmetros: begin, end e middle. Dependendo do nível de recursividade da execução do algoritmo, os valores dessas variáveis não estarão iguais as que estavam originalmente.


Método merge

Esse método realiza a ordenação. Recebe três parâmetros: a posição inicial, final e média do array.

  • Linha 31 até 33: Copia os valores do array original para o array auxiliar. Os valores copiados são limitados pelo intervalo definido pelas variáveis begin e end.
  • Linha 35 até 37: Declaro as variáveis i, j e k.
  • Linha 40 até 49: É aqui aonde a mágica acontece. A condição definido no loop while da linha 40 define que a iteração será feita do início da fatia esquerda até seu fim e depois do início da parte direita até seu fim. Essa condição é definida dessa forma porque sempre comparamos o grupo da esquerda com o da direita. Na imagem ou no gif animado é possível notar isso. Nas linhas 41 e 44, é avaliado quem é menor valor. Caso o valor da esquerda seja o menor, ele é copiado para o array original na posição certa (k) e a variável i é incrementada, caso contrário, o valor da direita é copiado e a variável j é incrementada. A variável k é incrementada fora do bloco condicional if pois, ele representa a posição final do valor ordenado logo, ele não precisa estar sendo incrementado onde é avaliado o menor valor e sim, depois que isso foi feito.
  • Linha 51 até 55: Nesse bloco são copiados os valores restantes da ordenação. O bloco acima não deixa o array original totalmente ordenado, ele só passa os menores valores para frente (ordenação crescente). Então, precisamos de uma outra iteração para recuperar os outros valores do array auxiliar e posicioná-los nas posições corretas. Como sabemos quais as posições corretas ? A variável k não é zerada logo, ele ainda guarda referência do último valor ordenado posicionado. As variáveis i e k devem ser incrementadas para realização do posicionamento.

Cumprimos mais uma etapa do nosso estudo. Entendemos a implementação do nosso programa. Vamos realizar alguns testes ?

Nossos testes serão feitos da seguinte forma: iremos montar um array um valores aleatórios e com as dimensões da primeira coluna. Depois de montado, iremos realizar a operação de ordenação 1 milhão de vezes e medir o tempo total. O tempo individual de cada processamento é calculado pela divisão do tempo total por 1 milhão.

Figura 4 - Testes feitos para medir a execução do algoritmo
Gráfico gerado a partir da planilha.

Figura 5 - Gráfico de quantidade de elementos por tempo
Através das evidências acima podemos notar que, conforme multiplicamos por 10 a quantidade de números, o resultado final também cresce da mesma forma. Esse algoritmo se mostrou eficaz para uma grande quantidade de números visto que, seu primo, Bubble Sort, demorou muito mais tempo.

Para fazer o download do algoritmo, clique aqui.

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários ou na nossa página do facebook.

Facebook: https://www.facebook.com/precisoestudarsempre/

Referências:
Merge sort - https://pt.wikipedia.org/wiki/Merge_sort
John von Neumann - https://pt.wikipedia.org/wiki/John_von_Neumann
Gif Merge Sort - https://upload.wikimedia.org/wikipedia/commons/c/cc/Merge-sort-example-300px.gif
Program: Implement merge sort in java. - http://java2novice.com/java-sorting-algorithms/merge-sort/
Mergesort in Java - Tutorial - http://www.vogella.com/tutorials/JavaAlgorithmsMergesort/article.html
Estruturas de dados e algoritmos em Java, Robert Lafore, 2004

Leia Mais ››

segunda-feira, 20 de julho de 2015

Conheça a verdade - Tabelas Hash(sem colisão)

Olá amigos do Preciso Estudar Sempre, meu nome é João Paulo e minha paixão é estudar. Hoje entraremos, mais uma vez, no grande reino das estruturas de dados. Para que possamos falar sobre esse assunto, é necessário que você, caro leitor, já possua conhecimentos sobre Java e OO.

Caso você precise muito baixar o projeto agora, clique aqui mas, se você aguenta mais um pouquinho, leia o post e baixe o projeto no link que tem lá embaixo, até porque dá uma trabalheira imensa trazer conteúdo de qualidade para vocês.

Voltando .... Imagine a seguinte situação: você possui um array com N elementos e quer procurar um elemento qualquer sendo que, você não sabe aonde ele está.

O que você faz ????

A primeira coisa que vem à nossa cabeça é uma busca sequencial, ou seja, verificar uma posição após a outra. Contudo, o custo de performance dessa busca é equivalente ao tamanho do array.

Nesse momento você pensa: "Ué, mas não existe a tal da busca binária, aquela que corta o array ao meio e elimina a outra metade ?"

Então, realmente existe a busca binária que faz isso tudo aí que você pensou mas, para a busca binária funcionar, ela tem como pré-requisito a ordenação do vetor, ou seja, você terá o custo da ordenação e da pesquisa. O ideal é podemos ter acesso direto ao elemento dentro do array.

É importante citar que nem sempre conhecemos a posição exata de um elemento dentro de um array logo, o acesso direto não é possível. Para tal, precisamos calcular essa posição, essa é a chave para a solução.

Como vamos calcular isso ?! E agora !?

Para que possamos contornar esse problema, precisamos sair da nossa zona de conforto, ou seja, dos arrays comuns que já conhecemos e precisamos aprender uma nova estrutura de dados, a tabela hash. Na tabela hash, não trabalhamos mais da forma tradicional, ou seja, de forma sequencial. Agora iremos espalhar nossos dados dentro de um array. Sim, você não leu errado, nós iremos espalhar nossos dados dentro de um array e, ainda te digo que isso permite que tenhamos o tão sonhado acesso direto.
Figura 1 - Comparativo entre tabela hash e lista

Nesse momento, imagino que você esteja realmente assustado.

Mas como iremos realizar esse espalhamento ? Vamos usar escolha randômica ? A resposta é não. Para conseguirmos espalhar com eficiência nossos dados, iremos utilizar uma função de espalhamento.

A função de espalhamento gera a posição em que vamos inserir nosso valor dentro do array. Essa posição é gerada através de uma chave passada como parâmetro. Cada chave deve ser única porque, estamos construindo uma tabela de hash sem colisões, visando um ambiente onde cada chave gere uma posição diferente. Caso existam chaves iguais, iremos gerar colisões, ou seja, duas inserções para uma mesma posição. Após nossa função de espalhamento ter sido executada, nosso acesso será reduzido para O(1).

Para uma função ser considerada uma função de espalhamento, ela deve preencher alguns pontos obrigatórios:
  • Ser simples e barata de se calcular.
  • Garantir que chaves diferentes produzam posições diferentes.
  • Garantir que não há preferência por posições

Existem diversos tipos de funções de espalhamento já conhecidas, vou listar algumas:
  • Método da divisão (iremos usar esta)
  • Método da multiplicação
  • Método da dobra
  • String como chave

Não explicarei como funciona cada função porque iremos estender muito nosso post e você ficará cansado. Caso as funções de espalhamento tenham gerado em você um grande interesse, fique tranquilo pois, farei um post exclusivo para isso.

Já entendemos tudo o que precisamos entender para ir para prática. Então, vamos ?

Crie a classe Aluno.

 public class Aluno {  
      private int matricula;  
      private String nome;  
      private float nota1;  
      private float nota2;  
      private float nota3;  
      public Aluno(int matricula, String nome, float nota1, float nota2,  
                float nota3) {  
           super();  
           this.matricula = matricula;  
           this.nome = nome;  
           this.nota1 = nota1;  
           this.nota2 = nota2;  
           this.nota3 = nota3;  
      }  
      //gets e sets  
 }  

A classe Aluno é uma classe simples, não preciso me estender em explicações sobre ela. Próximo passo.

Crie a classe HashTable.

 public class HashTable<T> {  
      private int qtd;  
      private int size;  
      private T[] objs;  
      private Set<Integer> keys;  
      public HashTable(int size) {  
           if(size == 0){  
                throw new IllegalArgumentException("O tamanho deve ser maior que 0");  
           }  
           this.size = size;  
           this.criarHashTable();  
      }  
      private void criarHashTable(){  
           this.objs = (T[]) new Object[this.size];  
           this.keys = new HashSet<Integer>();  
      }  
      public void add(T obj, int key){  
           if(this.qtd == this.size){  
                throw new IllegalStateException("Tabela cheia.");  
           }  
           int pos = this.chaveDivisao(key);  
           this.keys.add(key);  
           objs[pos] = obj;  
           qtd++;  
      }  
      private int chaveDivisao(int key) {  
           return (key & 0x7FFFFFFF) % size;  
      }  
      public T get(int key){  
           if(objs.length == 0){  
                return null;  
           }  
           int pos = this.chaveDivisao(key);  
           if(objs[pos] == null){  
                return null;  
           }  
           return objs[pos];  
      }  
      public T remove(int key){  
           if(objs.length == 0){  
                return null;  
           }  
           int pos = this.chaveDivisao(key);  
           if(objs[pos] == null){  
                return null;  
           } else {  
                T obj = objs[pos];  
                objs[pos] = null;  
                qtd--;  
                return obj;  
           }  
      }  
      public int size(){  
           return this.qtd;  
      }  
      public boolean isEmpty(){  
           return this.qtd == 0 ? true : false;  
      }  
      public Set keys(){  
           return this.keys;  
      }  
 }  

Vou ser pontual nas minhas explicações. Precisamos entender primeiramente o método criarHashTable(). Esse método é simples, ele inicia o array interno, o Set de keys interno e, é chamado dentro do construtor da HashTable. O Set de chaves foi criado visando a obtenção das mesmas para iteração de valores.

Note que o construtor da classe acima recebe um parâmetro o qual, representa o tamanho total da nossa HashTable. Além dos atributos objs keys os quais, já comentamos no parágrafo acima, também temos os atributos qtd size os quais, representam respectivamente, a quantidade de elementos presente no array e o tamanho total do array.

IMPORTANTE: Ao escolher o tamanho da HashTable, dê preferência a números primos pois, reduzem a probabilidade de colisões, mesmo a função de espalhamento não sendo muito eficaz. Caso você escolha um tamanho que seja potência de dois, a velocidade da HashTable será aprimorada mas a probabilidade de colisões pode aumentar caso a função de espalhamento seja muito simples.

A função add trabalha de forma bem simples. Ela calcula a posição baseada na chave a qual, é passada como parâmetro, insere o elemento naquela posição e incrementa o contador. A função remove trabalha de forma semelhante mas, antes de remover, verifica se existe algum valor naquela posição do array. Caso haja, remove o elemento, decrementa o contador e retorna o elemento removido.

A função get calcula a posição do elemento procurado através da chave passada e retorna o valor contido dentro do array naquela posição.

Uma das vantagens da HashTable, é que ela é muito fácil de se implementar.

Crie a classe Principal.

 public class Principal {  
      public static void main(String[] args) {  
           Aluno aluno1 = new Aluno(259379, "João Paulo Maida", 10, 7, 8);  
           Aluno aluno2 = new Aluno(1145, "Bruno Souza", 8, 9, 3);  
           Aluno aluno3 = new Aluno(55712, "Raphael Marques da Silva", 6, 7, 1);  
           Aluno aluno4 = new Aluno(19, "José da Silva", 1, 2, 3);  
           Aluno aluno5 = new Aluno(4754, "Luís Marcelo", 8, 5, 10);  
           Aluno aluno6 = new Aluno(4753, "Nathan Paulo Souza", 8, 5, 10);  
           Aluno aluno7 = new Aluno(1, "Beatriz Luana Campos", 7, 1, 2);  
           Aluno aluno8 = new Aluno(32, "Isadora Fernanda Monteiro", 6, 7, 7.5f);  
           Aluno aluno9 = new Aluno(0, "Marina Bruna da Silva", 2.5f, 3.5f, 8);  
           Aluno aluno10 = new Aluno(896358, "Levi Murilo Souza", 4.5f, 3, 9.7f);  
           
           HashTable<Aluno> hashTable = new HashTable<Aluno>(2048);  
           hashTable.add(aluno1, aluno1.getMatricula());  
           hashTable.add(aluno2, aluno2.getMatricula());  
           hashTable.add(aluno3, aluno3.getMatricula());  
           hashTable.add(aluno4, aluno4.getMatricula());  
           hashTable.add(aluno5, aluno5.getMatricula());  
           hashTable.add(aluno6, aluno6.getMatricula());  
           hashTable.add(aluno7, aluno7.getMatricula());  
           hashTable.add(aluno8, aluno8.getMatricula());  
           hashTable.add(aluno9, aluno9.getMatricula());  
           hashTable.add(aluno10, aluno10.getMatricula());  
           
           Aluno alunoRemovido = hashTable.remove(aluno1.getMatricula());  
           System.out.println(hashTable.size());  
           Aluno alunoRecuperado = hashTable.get(111);  
           System.out.println(alunoRecuperado);  
           Aluno alunoRecuperado2 = hashTable.get(55712);  
           
           System.out.println("Nome: " + alunoRecuperado2.getNome());  
           System.out.println("Matrícula: " + alunoRecuperado2.getMatricula());  
           System.out.println("Notas: " + alunoRecuperado2.getNota1() + "," + alunoRecuperado2.getNota2() + "," + alunoRecuperado2.getNota3());  
           System.out.println(hashTable.keys());  
      }  
 }  

A classe Principal só cria alunos e utiliza nossa API.

Pronto !! Chegamos ao fim de mais um post !!!!

Para baixar o projeto, clique aqui.

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários ou na nossa página do facebook.

Facebook: https://www.facebook.com/precisoestudarsempre/

Referências:
Aula 89 - Tabela Hash - Definição - https://www.youtube.com/watch?v=njkANXEMHTY
Aula 90 - Tabela Hash - Implementação - https://www.youtube.com/watch?v=K40yG9bmVZ4
Aula 91 - Tabela Hash - Criando e Destruindo a Tabela - https://www.youtube.com/watch?v=X55Ku_Mpw5g
Aula 92 - Função de Hashing - https://www.youtube.com/watch?v=o0TXB3QPOWY
Aula 93 - Tabela Hash - Inserção e busca sem tratamento de colisões -https://www.youtube.com/watch?v=sYKarxRQ_-g

Leia Mais ››

sexta-feira, 23 de janeiro de 2015

Árvore binária - Implementação em Java

Olá amados leitores. Hoje iremos estudar um tema muito interessante nas estruturas de dados, as árvores binárias. Para você que já conhece toda a parte teórica da coisa e só quer um exemplo prático, você pode clicar aqui e baixar um projeto pronto.

Ahhhhhhhhhhhhhhhhh, antes que alguém me pergunte: "Ainnn João, você vai falar sobre balanceamento ?"

Eu já respondo: "Não vou falar porque senão o post vai ficar extremamente extenso". Você vai ter que correr por fora amigo.

Vamos começar então ?!

Você sabe o que é uma árvore binária ? 

Não !?

Tudo bem amigo, vamos facilitar um pouco mais a pergunta. To pegando pesado !

Você sabe o que é um árvore (não me refiro a planta) ?

Também não !?

Bem, então, senta aí que lá vem história. Pegue seu café e acenda seu cigarro. Uma árvore, não é nada mais nada menos que, um grafo acíclico. Mas aí você me pergunta: O que é um grafo ?

Vamos descer mais um degrau. Segundo à Wikipedia, a teoria dos grafos é um ramo da matemática que estuda as relações entre os objetos de um determinado conjunto. Para representar tais relações, são usados estruturas chamadas grafos, G(V,A) onde, G é um conjunto de vértices e arestas. Os vértices são um conjunto não vazio dos vértices do grafo. As arestas são um conjunto de pares não ordenados de V. Vamos ao exemplo.

Figura 1 - http://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Tree_graph.svg/162px-Tree_graph.svg.png

Acima, temos o grafo G que é composto por V={1,2,3,4,5,6} e A={{1,4},{2,4},{4,3},{4,5},{5,6}}. Depois de toda essa explicação, você deve estar se perguntando: Para que esta porcaria serve ? Quem usa isso ?

Agora você vai ficar surpreso. Pesquise sobre o problema das sete pontes de Königsberg. Não irei falar sobre o problema das pontes aqui porque não é esse o nosso foco.

Lembra que no início do post, eu falei que uma árvore é um grafo acíclico ? Então, um grafo acíclico é aquele que não faz ciclos, ou seja, se no nosso exemplo acima, o vértice 1 estivesse ligado ao vértice 2, o grafo G não seria uma árvore pois, existiria um ciclo, 1 - 2 - 4.

Agora que, já sabemos o que é um grafo e o que é uma árvore. Vamos falar de suas propriedades:
  • O primeiro nó de uma árvore é o nó raiz.
  • Nó folha é todo nó que não possui conexões.
  • Em uma árvore existe 1 e somente 1 caminho que liga um nó ao outro.
  • Dado um determinado vértice, cada "filho" seu é a "raíz" de uma nova "sub-árvore".
  • Grau de um vértice é o número de sub-árvores do vértice.
  • Altura da árvore é o comprimento do caminho mais longo da raiz até uma das suas folhas.
  • O nível de um nó é o número de nós no caminho entre o vértice e a raiz.

Precisamos saber o que é uma árvore binária. Afinal de contas, estamos aqui para isso. As árvores binárias são um tipo de árvore onde, cada um de seus vértices pode possuir no máximo duas sub-árvores. O grau (número de filhos) de cada vértice pode ser 0, 1 ou 2.

Figura 2

Acima, temos um exemplo de árvore binária. Neste momento, não se preocupe em entender como os valores foram gravados ali. Veremos isto adiante. Vamos a melhor parte do post, vamos implementar !!!

Usaremos uma lista encadeada para construir nossa árvore por questões de performance. Se você não sabe o que é uma lista encadeada, eu recomendo que você dê uma olhada nesse post:


Vamos primeiro criar métodos para retornar se a árvore está vazia ou não, a quantidade de nós da árvore, altura da árvore e um método que imprima todos os valores da árvore. A forma que a árvore é percorrida é em pós-ordem pois, dessa forma é garantido que irei percorrer todos os filhos de um nó primeiro e só depois irei acessar o nó pai. Dessa forma, realizo as operações na árvore de um jeito mais seguro.

Crie as seguintes classes:

package pkg;
public class Node {
    private Integer valor;
    private Node noEsquerda;
    private Node noDireita;  
    public Node() { }
  
  public Node(Integer valor) {
        super();
        this.valor = valor;
    }
    public Integer getValor() {
        return valor;
    }
    public void setValor(Integer valor) {
        this.valor = valor;
    }
    public Node getNoEsquerda() {
        return noEsquerda;
    }
    public void setNoEsquerda(Node noEsquerda) {
        this.noEsquerda = noEsquerda;
    }
  public Node getNoDireita() {
        return noDireita;
    }
    public void setNoDireita(Node noDireita) {
        this.noDireita = noDireita;
    }
    @Override
    public String toString() {
        return "Node [valor=" + valor + "]";
    }    
}

package pkg;

public class BinaryTree {
    private Node root;

    public boolean isEmpty(){
        if(root == null){
            return true;
        }
        return false;
    }
    
    public int getAltura(){
        return getAltura(this.root);
    }
    
    private int getAltura(Node root){
        if(root == null){
            return 0;
        }
        int altEsq = getAltura(root.getNoEsquerda());
        int altDir = getAltura(root.getNoDireita());
        if(altEsq > altDir){
            return altEsq + 1;
        } else {
            return altDir + 1;
        }
    }
    
    public int getQtdNode(){
        return getQtdNode(root);
    }
    
    private int getQtdNode(Node root){
        if(root == null){
            return 0;
        }
        int qtdNodeEsq = getQtdNode(root.getNoEsquerda());
        int qtdNodeDireita = getQtdNode(root.getNoDireita());
        return qtdNodeEsq + qtdNodeDireita + 1;
    }
    
    public void imprimirArvore(){
        if(this.root == null)
            System.out.println("Árvore vazia");
        else
            imprimirArvore(this.root);
    }
    
    private void imprimirArvore(Node node){
        if(node.getNoEsquerda() != null){
            imprimirArvore(node.getNoEsquerda());
        }
        if (node.getNoDireita() != null){
            imprimirArvore(node.getNoDireita());
        }
        System.out.println("Nó: " + node.getValor());
    }
    
    public void inserir(int valor){
        inserir(this.root, valor);
    }
    
    public void inserir(Node node, int valor) {
        if(this.root == null){
            this.root = new Node(valor);
        } else {
            if (valor < node.getValor()) {
                if (node.getNoEsquerda() != null) { 
                    inserir(node.getNoEsquerda(), valor); 
                } else { 
                    //Se nodo esquerdo vazio insere o novo no aqui 
                    node.setNoEsquerda(new Node(valor)); 
                } 
                //Verifica se o valor a ser inserido é maior que o no corrente da árvore, se sim vai para subarvore direita 
            } else if (valor > node.getValor()) { 
                //Se tiver elemento no no direito continua a busca 
                if (node.getNoDireita() != null) { 
                    inserir(node.getNoDireita(), valor); 
                } else {
                    //Se nodo direito vazio insere o novo no aqui 
                    node.setNoDireita(new Node(valor)); 
                } 
            }
        }
    }
    
    public Node remover(int valor) throws Exception{
        return remover(this.root, valor);
    }
    
    private Node remover(Node node, int valor) throws Exception{
        if(this.root == null){
            throw new Exception("Árvore vazia");
        } else {            
            if(valor < node.getValor()){
                node.setNoEsquerda(remover(node.getNoEsquerda(), valor));
            } else if(valor > node.getValor()){
                node.setNoDireita(remover(node.getNoDireita(), valor));
            } else if (node.getNoEsquerda() != null && node.getNoDireita() != null) {
                /*2 filhos*/  
                System.out.println("  Removeu No " + node.getValor());
                node.setValor(encontraMinimo(node.getNoDireita()).getValor());
                node.setNoDireita(removeMinimo(node.getNoDireita()));
            } else {  
                System.out.println("  Removeu No " + node.getValor());  
                node = (node.getNoEsquerda() != null) ? node.getNoEsquerda() : node.getNoDireita();  
            }  
            return node;
        }
    }
    
    private Node removeMinimo(Node node) {  
        if (node == null) {  
            System.out.println("  ERRO ");  
        } else if (node.getNoEsquerda() != null) {  
            node.setNoEsquerda(removeMinimo(node.getNoEsquerda()));  
            return node;  
        } else {  
            return node.getNoDireita();  
        }  
        return null;  
    }  
  
    private Node encontraMinimo(Node node) {  
        if (node != null) {  
            while (node.getNoEsquerda() != null) {  
                node = node.getNoEsquerda();  
            }  
        }  
        return node;  
    }
}

Agora que você já criou as classes. Vamos entender os métodos da classe BinaryTree.

Método isEmpty():

Esse método é o mais simples de todos. Para saber se a árvore está vazia ou não, precisamos somente saber se seu root é nulo. Caso ele seja, a árvore estará vazia pois, no root é onde começam as conexões com outros nós.

Método getAltura()getAltura(Node root):

Esses métodos são sobrecarregados pois, o segundo é recursivo. A recursividade do segundo método se dá pelo fato de que, quando estamos trabalhando com árvore e precisamos percorrê-las, temos que chegar até sua folha da esquerda, depois chegar até sua folha da direita e por último analisar a raiz. Sem recursividade isso não seria possível. Para descobrir a altura, precisamos primeiro descobrir a altura do nó da esquerda, depois do nó da direita e somar 1. É necessário somar 1 pois a altura dos nós folha é 0. 

Precisamos comparar se a altura da esquerdá é maior do que a da direita porque, a altura de uma árvore é o maior caminho da raiz até a folha mais longe. Logo, quanto maior a altura do nó mais longe ele está da raiz. Caso você ainda tenha dúvidas para entender essa função, recomendo que você assita esse vídeo: https://www.youtube.com/watch?v=qVnNdmx4fOA

Método getQtdNode()getQtdNode(Node root):

Esses métodos são sobrecarregados pelo mesmo motivo que o método getAltura() é sobrecarregado também. Para descobrir a quantidade de nós da árvore, precisamos descobrir a quantidade de nós a esquerda e à direita e somar 1. Somar 1 significa que estamos levando em conta o nó raiz.

Método imprimirArvore()imprimirArvore(Node node):

Imprime a árvore de forma pós-ordem, ou seja, acessa primeiro nó esquerdo, depois nó direito e por último a raiz. Para cada nó, seu valor é impresso no console.

Tcharammmmmmmm !!! 

Já entendemos quatro métodos da nossa API de árvore binária. Faltam os métodos para inserir e remover. Para realizarmos isso precisamos entender um novo conceito: árvore binária de busca. A árvore binária de busca é um tipo de árvore binária que estabelece que todos os valores à esquerda do nó pai devem ser menores que o mesmo e todos os valores à direita devem ser maiores que o nó pai. Entendeu agora porque eu não quis entrar em detalhes na figura 2 ?

As inserções e remoções devem respeitar essa regra. Caso contrário, a árvore não será mais uma árvore binária de busca.

Falarei de inserção e remoção de valores em árvores binária de busca porque, é o que é mais feito pelos autores em livros. Se quiséssemos estudar a inserção e remoção em árvore binárias comuns, precisaríamos somente saber os conceitos básicos dessa árvore. Dessa forma, você em 1 post aprende dois tipos de árvores.

Método inserir(int valor) e inserir(Node node, int valor):
  1. Começo a percorrer a árvore pela raiz
  2. Avalio se o valor que está sendo inserido é maior ou menor (não pode ser igual) do que o nó que estou avaliando.
  3. Caso seja menor, avalio se já existe um nó na esquerda e repito o passo 2.
  4. Caso não haja um nó esquerdo, crio um e gravo o novo valor ali.
  5. Caso o valor seja maior do que o nó que estou avaliando, avalio se já existe um nó na direita e repito o passo 2.
  6. Caso não haja um nó direito, crio um e gravo o novo valor ali.

Método remover(int valor)remover(Node node, int valor):
  1. Avalio se a raíz é null. Caso seja informo que a árvore está vazia.
  2. Caso a raíz não seja null. Avalio se o valor que quero remover é maior ou menor que o nó que estou avaliando (o primeiro nó é a raíz, obviamente).
  3. Caso seja menor, acesso o nó esquerdo e faço isso até encontrá-lo.
  4. Quando encontro ele, avalio se ele tem filhos à esquerda e à direita.
  5. Caso tenha, vou para a sub-árvore da direita e procuro o nó de menor valor à esquerda. Depois de encontrá-lo, atribuo o valor desse menor nó ao nó que está sendo excluído (ver imagem 5).
  6. Depois da atribuição do passo 5 ter sido feita, eu preciso refazer a ligação que o pai do menor nó à esquerda tinha pois, agora o pai dele vai apontar para null.
  7. O valor passado como parâmetro foi excluído.
  8. Ao contrário do passo 3, ou seja, o valor é maior, acesso o nó direito e faço isso até encontrá-lo.
  9. Repito os passos 4 à 7.
  10. Caso um nó seja folha ou só tenha 1 filho, removo seu valor e atualizo seu pai.
Para ficar mais fácil, olhe as imagens 3, 4 e 5.

Figura 3 - Excluindo nó folha - http://upload.wikimedia.org/wikipedia/commons/6/6c/Bstreedeleteleafexample.jpg

Figura 4 - Excluindo nó com 1 filho - http://upload.wikimedia.org/wikipedia/commons/6/6a/Bstreedeleteonechildexample.jpg

Figura 5 - Excluindo um nó com dois filhos - http://upload.wikimedia.org/wikipedia/commons/1/15/Bstreedeletenotrightchildexample.jpg


Bem amigos, esse post ficou grande mas, espero que esteja tudo bem explicado.

Terminamos !! Ufa !! 

Sugestões ?! Críticas ?! Elogios ?! 

Deixe aí nos comentários ou na nossa página do facebook.



Referências:
[ED] Aula 69 - Árvore Binária: Definição - https://www.youtube.com/watch?v=9WxCeWX9qDs
[ED] Aula 70 - Árvore Binária: Implementação - https://www.youtube.com/watch?v=TR8ZLUKmcPc
[ED] Aula 72 - Árvore Binária: informações básicas - https://www.youtube.com/watch?v=qVnNdmx4fOA
[ED] Aula 73 - Percorrendo uma Árvore Binária - https://www.youtube.com/watch?v=z7XwVVYQRAA
[ED] Aula 74 - Árvore Binária de Busca - https://www.youtube.com/watch?v=M7cb4HjePJk
[ED] Aula 75 - Inserção em Árvore Binária de Busca - https://www.youtube.com/watch?v=8cdbmsPaR-k
[ED] Aula 76 - Remoção em Árvore Binária de Busca - https://www.youtube.com/watch?v=_0Yu9BSYXGY
http://javafree.uol.com.br/topic-882029-Arvore-binaria.html
Leia Mais ››

terça-feira, 23 de dezembro de 2014

Listas encadeadas em Java

Olá caros leitores do blog Preciso Estudar Sempre, o tópico desta semana será sobre listas encadeadas. Escolhi esse tema porque acredito que é importante e porque não me lembro de ter falado sobre tal antes.

O conhecimento deste assunto é de grande importância no desenvolvimento de software pois, as listas encadeadas possuem uma estrutura bem diferente das listas tradicionais. Tal estrutura que pode nos ajudar em certas situações.

Antes que você se impressione com o assunto, eu já digo: "Fique tranquilo". Este tema é de fácil entendimento. Porém, é necessário de um pouco de conhecimento de programação.

Para os que já conhecem o assunto ou que já ouviram falar, já aviso que não falarei sobre listas duplamente encadeadas ou listas circulares porque senão o post ficará muito extenso e massante.

No Java, já existem estruturas prontas para listas encadeadas. Estou falando da LinkedList, clique aqui para ver a documentação. Mas, nesse post iremos criar nossa própria estrutura de lista encadeada.

Para realizar este estudo não é necessário ter uma IDE super moderna. Se você quiser usar o notepad não tem problema.

Primeiro, vamos entender o conceito das listas encadeadas. As listas tradicionais tem suas células organizadas todas juntas e posicionadas uma atrás da outra. Isso lhes garante velocidade em buscas e acesso randômico, ou seja, eu posso acessar qualquer posição que eu queira. Porém, existe uma desvantagem nesta estrutura. Quando eu preciso inserir ou remover elementos no meio da lista, o trabalho para reconstruir a lista com todos elementos é muito grande visto que, eu preciso remover os elementos que virão após o novo/removido elemento e depois recolocá-los, formando assim uma lista maior/menor.

http://voidexception.weebly.com/uploads/1/1/9/4/11944659/4652147_orig.jpg

Com as listas encadeadas não existe este problema porque suas células são organizadas de forma diferente. Nas listas tradicionais, cada elemento não tem conhecimento do próximo elemento ou do anterior porque estão todas juntas dentro da estrutura. Nas lista encadeadas, não. Cada elemento está "solto" na memória. Então, agora, você fica com aquela dúvida.

"Se os elementos estão soltos, como a lista é montada ? "

A resposta é simples, cada elemento possui uma referência somente para o próximo elemento. Como se formassem uma corrente, como se estivessem ligados (daí que vem o nome Linked List). Na figura abaixo, temos um exemplo perfeito de como é essa lista.

http://www.ime.usp.br/~pf/algoritmos/xfig/lista2a.gif

Os elos da lista são formados por ponteiros que possuem a referência para o próximo elemento. Quando não existe a referência para o próximo elemento, o ponteiro assume o valor nulo. Abaixo, temos uma relação de vantagens e desvantagens dessa estrutura.

Vantagens:
  • Linked lists são estruturas de dados dinâmicos que alocam a memória necessária enquanto o programa está funcionando.
  • Operações de inserir e deletar células (nodes) são facilmente implementadas.
  • Estruturas de dados lineares tais como, pilhas e filas são facilmente executadas com uma lista encadeada.
  • Eles podem reduzir o tempo de acesso e podem aumentar em tempo real sem overhead de memória.
Desvantagens:
  • Tem tendência de desperdiçar memória pelo fato dos ponteiros pediram novo espaço de armazenamento.
  • Nós em uma lista encadeada devem ser lidos do início.
  • Dificuldades surgem em listas encadeadas quando se tente percorrê-la de trás para frente. Tal tarefa é extremamente complicado. Adicionar um backpointer (ponteiro de ré) causa desperdício de memória.
Agora que já sabemos como a lista encadeada funciona, vamos ao exemplo prático. Nós criaremos uma lista encadeada que representa uma escala de indicação de empregos, ou seja, uma pessoa indica a outra para uma vaga de emprego mas, ela só pode indicar 1 pessoa e só conhece a pessoa que indicou.

Crie a classe Pessoa. Esta classe representa a pessoa.

 package pkg;  

 public class Pessoa { 
      private Integer id; 
      private String nome; 
      private String email; 
      public Pessoa() { 

      } 

      public Pessoa(Integer id, String nome, String email) { 
           this.id = id; 
           this.nome = nome; 
           this.email = email; 
      } 

      public Integer getId() { 
           return id; 
      } 

      public void setId(Integer id) { 
           this.id = id; 
      } 

      public String getNome() { 
           return nome; 
      } 

      public void setNome(String nome) { 
           this.nome = nome; 
      } 

      public String getEmail() { 
           return email; 
      } 

      public void setEmail(String email) { 
           this.email = email; 
      } 

      @Override 
      public String toString() { 
           return "Pessoa [id=" + id + ", nome=" + nome + ", email=" + email + "]"; 
      } 
 } 

Crie a classe Celula. Esta classe representa a célula (posição ou node) da lista. Como vimos acima, cada célula precisa da referência do próximo e isso é feito através do atributo proximo.

 package pkg;  

 public class Celula { 
      private Celula proximo; 
      private Pessoa valor; 
      public Celula getProximo() { 
           return proximo; 
      } 

      public void setProximo(Celula proximo) { 
           this.proximo = proximo; 
      } 

      public Pessoa getValor() { 
           return valor; 
      } 

      public void setValor(Pessoa valor) { 
           this.valor = valor; 
      } 
 } 

Crie a classe ListaEncadeada. Esta classe representa a lista encadeada. Note que ela não guarda suas células em nenhum tipo de array ou vetor. As únicas referências que ela tem para suas células são os atributos primeiro e ultimo. A partir deste dois atributos, será realizada as operações na lista. O atributo posicaoAtual é usado para as operações iteração e recuperação do objeto do laço.

 package pkg;  
 public class ListaEncadeada { 
      private Celula primeiro; 
      private Celula ultimo; 
      private Celula posicaoAtual; 
      /** 
       * Adiciona uma pessoa no fim da lista. 
       * @param valor 
       */ 
      public void adicionar(Pessoa valor){ 
           Celula celula = new Celula(); 
           celula.setValor(valor); 
           if(primeiro == null && ultimo == null){ 
                primeiro = celula; 
                ultimo = celula; 
           } else { 
                ultimo.setProximo(celula); 
                ultimo = celula; 
           } 
      } 
      /** 
       * Remove uma pessoa do fim da lista. 
       */ 
      public void remover(){ 
           if(primeiro.getProximo() != null){ 
                Celula celula = this.recuperarPenultimo(this.primeiro); 
                ultimo = celula; 
                celula.setProximo(null); 
           } else { 
                primeiro = ultimo = null; 
           } 
      } 
      /** 
       * Recupera o penultimo elemento da lista 
       * @param celula 
       * @return 
       */ 
      private Celula recuperarPenultimo(Celula celula){ 
           if(celula.getProximo().equals(ultimo)){ 
                return celula; 
           } 
           return recuperarPenultimo(celula.getProximo()); 
      } 
      public boolean temProximo(){ 
           if(primeiro == null){ 
                return false; 
           } else if (posicaoAtual == null){ 
                posicaoAtual = primeiro; 
                return true; 
           } else { 
                boolean temProximo = posicaoAtual.getProximo() != null ? true : false; 
                posicaoAtual = posicaoAtual.getProximo(); 
                return temProximo; 
           } 
      } 
      public Celula getPosicaoAtual(){ 
           return this.posicaoAtual; 
      } 
 } 

Agora, crie a classe Principal. Esta classe contém o método main e a partir dele, chamamos os método da lista.

 package pkg;  
 public class Principal { 
      public static void main(String[] args) { 
           ListaEncadeada listaEncadeada = new ListaEncadeada(); 
           Principal principal = new Principal(); 
           principal.adicionarPessoa(listaEncadeada); 
           principal.remover(listaEncadeada); 
           while(listaEncadeada.temProximo()){ 
                System.out.println(listaEncadeada.getPosicaoAtual().getValor()); 
           } 
      } 
      private void adicionarPessoa(ListaEncadeada listaEncadeada){ 
           Pessoa p1 = new Pessoa(1, "João", "jp@gmail.com"); 
           Pessoa p2 = new Pessoa(2, "Maria", "maria@gmail.com"); 
           Pessoa p3 = new Pessoa(3, "Bruno", "bruno@gmail.com"); 
           Pessoa p4 = new Pessoa(4, "José", "jose@gmail.com"); 
           Pessoa p5 = new Pessoa(5, "Mário", "mario@gmail.com"); 
           Pessoa p6 = new Pessoa(6, "Eduardo", "dudu@gmail.com"); 
           listaEncadeada.adicionar(p1); 
           listaEncadeada.adicionar(p2); 
           listaEncadeada.adicionar(p3); 
           listaEncadeada.adicionar(p4); 
           listaEncadeada.adicionar(p5); 
           listaEncadeada.adicionar(p6); 
      } 
      private void remover(ListaEncadeada listaEncadeada){ 
           listaEncadeada.remover(); 
           listaEncadeada.remover(); 
           listaEncadeada.remover(); 
      } 
 } 

Não entrei muito a fundo nas operações que são possíveis realizar em uma lista encadeada porque, eu ficaria um mês todo escrevendo este post e no fim disponibilizaria uma API muito extensa. O meu intuito com esse post é apenas dar um guia introdutório do assunto visando, dar insumos suficientes para que os leitores possa andar "pelas suas próprias pernas".

Para baixar o projeto completo, clique aqui.

Sugestões ? Críticas ? Elogios ? Deixe aí nos comentários ou na página do facebook.

https://www.facebook.com/precisoestudarsempre/

Referências:
http://www.ime.usp.br/~pf/algoritmos/aulas/lista.html
http://en.wikipedia.org/wiki/Linked_list
http://docs.oracle.com/javase/7/docs/api/java/util/LinkedList.html
Leia Mais ››