Busca é um método bastante utilizado em programação, pois frequentemente necessitamos de informações que podem ser adquiridas através dela.
Pode-se realizar busca em diversos lugares como: vetores (verificação da existência de um valor dentro dele), intervalos numéricos (qual o melhor ponto dentro de um intervalo que satisfaz o problema), strings (se um letra está contida em uma string) e etc.
Trataremos neste capítulo da pesquisa sequencial e da pesquisa binária.
A ordenação consiste em dispor elementos em uma certa sequência, seguindo algum critério. Por exemplo, a ordenação lexicográfica (alfabética) para dados literais, ou crescente e decrescente para dados numéricos.
Existem muitos algoritmos de ordenação conhecidos: InsertionSort, ShellSort, BubbleSort, HeapSort, MergeSort , QuickSort, dentro Outros.
Aqui veremos o BubbleSort. Outros algoritmos de ordenação serão vistos na matéria de Estruturas de Dados.
Vamos supor que temos 4 portas fechadas, e atrás de cada porta contém 1 número, qual estratégia poderíamos tomar para encontrar o número 3?
O método trivial seria abrir qualquer porta aleatoriamente enquanto não encontrar o número 3.
Obviamente este método não é nem de perto o mais eficiente.
Outra estratégia seria começando na primeira porta e ir abrindo as próximas na sequência. Este método se chama Busca Linear.
Verificamos sequencialmente (ou seja, um após o outro) cada elemento. Se encontramos o valor desejado, então a pesquisa foi bem sucedida.
Caso todos os elementos do conjunto sejam verificados e o elemento desejado não esteja entre eles, dizemos que a pesquisa foi mal sucedida.
Download Code Code Execution
- int i, valor, achou;
- int conjunto[] = {3,6,7,10,4,12,9,5,8};
- valor = 5;
- i = 0;
- achou = 0;
- while ((i < 10) && (!achou)) {
- if (conjunto[i] == valor)
- achou = 1;
- else
- i++;
- }
- if (achou)
- printf("%d encontrado na posicao %d.\n", valor, i + 1);
- else
- printf("%d nao encontrado.\n", valor);
A pesquisa binária utiliza a técnica de “dividir e conquistar”.
Primeiro, testamos se o elemento procurado é menor que o elemento do meio do vetor. Se for o caso, então passamos a buscar apenas na primeira metade do vetor.
Se não, testamos se o elemento procurado é maior que o elemento do meio do vetor. Se for o caso, então passamos a buscar apenas na segunda metade do vetor.
Caso contrário o valor procurado é igual ao elemento que está no meio do vetor.
Suponha que desejamos buscar o número 6 no mesmo vetor anterior, porém agora foi informado que o vetor está ordenado em ordem crescente, como aplicar a busca binária ?
Observação 1: O vetor precisa estar ordenado para conseguir realizar a busca binária.
Observação 2: Note que, a cada teste, descartamos uma das metades do (sub)vetor pesquisado.
Download Code Code Execution
- int inicio, fim, meio, valor, achou;
- int conjunto[] = {3,4,5,6,7,8,9,10,12};
- valor = 6;
- inicio = 0;
- fim = 8;
- achou = 0;
- while ((inicio <= fim) && (!achou)) {
- meio = (inicio + fim) / 2;
- if (valor < conjunto[meio])
- fim = meio - 1;
- else if (valor > conjunto[meio])
- inicio = meio + 1;
- else
- achou = 1;
- }
- if (achou)
- printf("%d encontrado na posicao %d.\n", valor, meio + 1);
- else
- printf("%d nao encontrado.\n", valor);
Resumindo, a eficiência da busca binária é muito superior a uma busca linear (no pior caso), pois a cada iteração metade do sub-vetor é descartada. Sua complexidade é de O(log(n)) enquanto a complexidade da busca linear é O(n) (Assunto sobre complexidade será tratado futuramente).
Segue um exemplo da comparação entre as duas buscas.
Vamos analisar um exemplo de funcionamento do algoritmo antes de mostrá-lo. Suponha um vetor de inteiros com n elementos que queremos ordenar em ordem crescente.
Download Code Code Execution
- void bubbleSort(int v[], int n){
- int i, j;
- // passo 3
- for(j = 0; j < n; j++){
- // passo 2
- for(i = 0; i < n-1; i++){
- // passo 1
- if(v[i] > v[i+1]){
- int aux = v[i];
- v[i] = v[i+1];
- v[i+1] = aux;
- }
- }
- }
- }
- int main(){
- int v[] = {-10, 2, 0, 4, 6, 2, -5, 20, 7, 9};
- int n = 10, i;
- printf("Vetor original com %d elementos\n", n);
- for(i = 0; i < n; i++)
- printf("%d ", v[i]);
- printf("\n");
- // ordenar o vetor
- bubbleSort(v, n);
- printf("Vetor ordenado com %d elementos\n", n);
- for(i = 0; i < n; i++)
- printf("%d ", v[i]);
- printf("\n");
- }
Pergunta: Se quisessemos organizar o vetor em ordem decrescente, o que precisaria ser mudado?
Note que o vetor, ao final da execução do algoritmo, está ordenado em ordem crescente, como era o objetivo. Este algoritmo vai funcionar para qualquer vetor de inteiros. No entanto, como cientistas da computação sempre devemos estar preocupados em criar programas funcionais e eficientes. Vamos analisar este algoritmo acima:
Com estas observações, podemos melhorar nosso algoritmo. A ideia básica continua sendo a mesma, mas a eficiência aumenta, pois melhoramos a performace dele para alguns casos mais comuns, adicionando as seguintes mudanças.
Download Code Code Execution
- void betterBubbleSort(int v[], int n){
- int i, j, ok = 0;
- // passo 3
- for(j = 0; j < n && ok == 0; j++){
- // passo 2
- ok = 1;
- for(i = 0; i < n-j-1; i++){
- // passo 1
- if(v[i] > v[i+1]){
- ok = 0;
- int aux = v[i];
- v[i] = v[i+1];
- v[i+1] = aux;
- }
- }
- }
- }
- int main(){
- int v[] = {-10, 2, 0, 4, 6, 2, -5, 20, 7, 9};
- int n = 10, i;
- printf("Vetor original com %d elementos\n", n);
- for(i = 0; i < n; i++)
- printf("%d ", v[i]);
- printf("\n");
- // ordenar o vetor
- betterBubbleSort(v, n);
- printf("Vetor ordenado com %d elementos\n", n);
- for(i = 0; i < n; i++)
- printf("%d ", v[i]);
- printf("\n");
- }
A mudança pode parecer pequena, mas aumenta bastante a eficiência do algoritmo. Por exemplo, repare que o primeiro BubbleSort, no link de execução
possui 283 passos, enquanto o segundo só possui 190, uma melhora de quase 33%.
Uma análise mais detalhada do BubbleSort está na seção Análise do BubbleSort.
Frequência de Números | Saltos Ornamentais | Troca de Cartas |
Frase Completa | Pares e Ímpares | A Biblioteca do Senhor Severino |
Assigning Teams | Onde está o Mármore? | Exame Geral |
A análise de algoritmos estuda a correção e o desempenho de algoritmos.
Em outras palavras, a análise de algoritmos procura respostas para perguntas do seguinte tipo:
"Este algoritmo resolve o meu problema?
Quanto tempo o algoritmo consome para processar uma 'entrada' de tamanho n?".
A análise feita é uma estimativa de tempo (e espaço) utilizado por um determinado algoritmo independente do hardware utilizado. Esta estimativa consistem em analisar o número de operações do algoritmo para diversos casos possíveis,
em geral o melhor caso, o caso médio e o pior caso.
Definimos uma operação como uma linha de código executada, e contamos quantas vezes cada linha é executada.
No algoritmo da busca linear, por exemplo, se existir um vetor com os números: 1, 46, 4, -1, 20, caso o número buscado seja o número 1, o programa irá executar apenas uma verificação, porém este é o melhor caso para a situação e não é assim que se avalia a complexidade de um algoritmo. Sendo assim, o método correto consiste em analizar no pior caso, ou seja, quando o número não pertence ao conjunto ou ele está na última posição do vetor, necessitando de 6 operações (n operações para um vetor de tamanho n) e assim sua complexidade é linear O(n), pois para um vetor de tamanho n será necessáario n operações para completar o algoritmo.
Com a análise das operações, chegamos a uma função que descreve o crescimento do tempo de execução do algoritmo. Em geral, esta função pode ser bastante complicada, e não é tão prática de se trabalhar, por isso usamos a notação assintótica. Nesta notação, consideramos apenas o termo de maior ordem. No caso da busca linear, seria o n.
A notação assintótica que veremos é a Big-O. Ele representa um limite assintótico superior, isto quer dizer que o algoritmo pode ter uma execução melhor do que a notação indica, mas no pior caso ela será a analisada. Como em geral estamos interessados na execução do algoritmo no pior caso, a notação Big-O é muito interessante. Assim, dizemos que o algoritmo de busca linear tem ordem de complexidade O(n).
Algoritmos com limite assintótico melhor são mais eficientes, e preferíveis a outros com limites maiores.
Primeiramente vamos recaptular os passos necessários para a execução do algoritmo.
Pegando só a função do BubbleSort, e considerando cada linha como uma operação, podemos ter uma estimativa da complexidade.
- void bubbleSort(int v[], int n){
- int i, j; // executado 1 vez
- // passo 3 - executado n vezes
- for(j = 0; j < n; j++){
- // passo 2 - executado n vezes
- for(i = 0; i < n-1; i++){
- // passo 1
- if(v[i] > v[i+1]){ // executado toda vez
- int aux = v[i];
- v[i] = v[i+1];
- v[i+1] = aux;
- }
- }
- }
- }
Portanto a estimativa seria da ordem de 1+ 2n² + α, onde α é a quantidade de vezes que as operações internas ao if do passo 1 são executadas.
Removendo as constantes e variáveis de menor ordem temos que a complexidade do BubbleSort, assintóticamente, é O(n²). Conforme vimos na Análise Inicial
este algoritmo terá o mesmo tempo para qualquer caso. As melhorias apresentadas naquela seção aceleram o algoritmo para alguns casos. O algoritmo melhorado é O(1) no melhor caso,
mas continua O(n²) no pior caso. Uma análise mais completa do algoritmo pode ser vista. aqui.
Algoritmos de ordenação com complexidade O(n²), por exemplo, BubbleSort, Insertion Sort, Selection Sort, etc não são usados em aplicações mais complexas, pois demoram demais para vetores grandes. Por exemplo, para vetores com 10 elementos o tempo seria 10² = 100 operações. Para vetores com 100 elementos, temos 100² = 10000 operações. Para vetores com 1000 elementos, 1000² = 1000000 operações. Um computador consegue executar, em geral, 108 operações em 1 segundo. Assim, este algoritmo demora muito para vetores com muito mais de 1000 elementos.