Algoritmos: busca binária (versão simplificada) 5/14/13 6:56 PM Projeto de Algoritmos Home | Prefácio | Livros | Sítios WWW | Índice Esta é uma versão simplificada da página. Veja também a versão completa. "Binary search is to algorithms what a wheel is to mechanics: It is simple, elegant, and immensely important." —Udi Manber, Introduction to Algorithms "Binary search is a notoriously tricky algorithm to program correctly. It took seventeen years after its invention until the first correct version of binary search was published!" —Steven Skiena, The Algorithm Design Manual Busca em vetor ordenado Esta página estuda o seguinte problema básico: determinar se um dado número está ou não está em um dado vetor ordenado. Mais precisamente, dado um número x e um vetor crescente v[0..n-1], encontrar um índice m tal que v[m] == x . É claro que o problema pode não ter solução. Este é o caso, por exemplo, se o vetor é vazio, ou seja, se n vale 0. 0 n-1 111 222 333 444 555 555 666 777 888 888 888 999 999 Vamos examinar dois algoritmos para o problema: um óbvio e lento e outro sofisticado e muito mais rápido. Veja o verbete Binary search algorithm na Wikipedia. Busca sequencial Comecemos com um algoritmo simples e óbvio: // // // // A função abaixo recebe um número x e um vetor crescente v[0..n-1]. Ela devolve um índice m em 0..n-1 tal que v[m] == x. Se tal m não existe, a função devolve -1. int buscaSequencial( int x, int n, int v[]) { int m = 0; while (m < n && v[m] < x) ++m; http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html Page 1 of 8 Algoritmos: busca binária (versão simplificada) 5/14/13 6:56 PM if (m < n && v[m] == x) return m; else return -1; } O valor de m muda a cada iteração, mas a relação v[m-1] < x é invariante: ela vale no início de cada iteração. Mais precisamente, essa relação invariante vale imediatamente antes da comparação de m com n. Ela vale até mesmo no começo da primeira iteração se estivermos dispostos a imaginar que v[-1] vale −∞. A relação invariante vale, em particular, no início da última iteração, quando m ≥ n ou v[m] ≥ x. Portanto, se a função devolve -1 então x é, de fato, diferente de todos os elementos do vetor v[0..n-1]. Quantas comparações de x com elementos de v essa função faz? A resposta, mesmo no pior caso, é n . O consumo de tempo da função é proporcional ao número de comparações. Se 1 microssegundo decorre entre duas comparações consecutivas, então uma busca em n elementos consome n microssegundos e uma busca em 8n elementos consome 8n microssegundos. É possível resolver o problema com menos comparações? Em outras palavras, é possível resolver o problema sem comparar x com cada um dos elementos do vetor? A resposta é afirmativa, como veremos adiante. Exercícios 1. Critique a seguinte versão da função buscaSequencial: int busca( int int m = 0; while (v[m] if (v[m] == else return } x, int n, int v[]) { < x && m < n) ++m; x) return m; -1; 2. Discuta a seguinte versão recursiva da função buscaSequencial: int busca2( int x, int n, int v[]) { if (n == 0) return -1; if (x == v[n-1]) return n-1; return busca2( x, n-1, v); } Busca binária http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html Page 2 of 8 Algoritmos: busca binária (versão simplificada) 5/14/13 6:56 PM Há um método de solução do problema que é muito mais eficiente que a busca sequencial. Ele se baseia na ideia comumente usada para encontrar um nome em uma lista telefônica. É claro que a ideia só faz sentido porque o vetor está ordenado. // // // // A função abaixo recebe um número x e um vetor crescente v[0..n-1]. Ela devolve um índice m tal que v[m] == x ou devolve -1 se tal m não existe. int buscaBinaria( int x, int n, int v[]) { int e, m, d; e = 0; d = n-1; while (e <= d) { m = (e + d)/2; if (v[m] == x) return m; if (v[m] < x) e = m + 1; else d = m - 1; } return -1; } Os nomes das variáveis não foram escolhidos por acaso: e lembra "esquerda", m lembra "meio" e d lembra "direita". O resultado da divisão por 2 na expressão (e+d)/2 é automaticamente truncado, pois e, d e 2 são do tipo int. Por exemplo, se e e d valem 0 e 5 respectivamente então (e+d)/2 vale 2. 0 e d n-1 111 222 333 444 555 555 666 777 888 888 888 999 999 A ideia do algoritmo de busca binária (= binary search) é um verdadeiro "ovo de Colombo". Ela é a base de muitos algoritmos eficientes para diversos problemas. Busca binária: a função está correta? Para compreender o algoritmo, basta verificar que no início de cada repetição do while, imediatamente antes da comparação de e com d, temos a seguinte relação invariante: v[e-1] < x < v[d+1] . Esse invariante vale até mesmo no início da primeira iteração, se imaginarmos que v[-1] vale −∞ e que v[n] vale +∞. Esse jogo de imaginação faz sentido porque o vetor é crescente. O invariante vale, em particular, no início da última iteração, quando e > d (mais precisamente, e == d+1). Nesse caso, nenhum elemento do vetor é maior que v[e-1] e menor que v[d+1], pois o vetor é crescente, Essa observação, junto com a relação invariante acima, garante que x é diferente de todos os elementos de v[0..n-1]. Conclusão: a resposta http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html Page 3 of 8 Algoritmos: busca binária (versão simplificada) -1 5/14/13 6:56 PM está correta nesse caso. O valor da diferença d - e diminui a cada iteração (cuidado: isso não é tão óbvio assim!). Portanto, a menos que tenhamos v[m] == x, a diferença fica negativa no início de alguma iteração e então a execução do while termina. Isso encerra a análise da correção do algoritmo. Busca binária: desempenho do algoritmo Quanto tempo o algoritmo leva para parar? No início da primeira iteração, d-e vale aproximadamente n. No início da segunda, vale aproximadamente n/2. No início da terceira, n/4. No início da (k+1)-ésima, n/2k. Quando k passar de log2 n, o valor da expressão n/2k fica menor que 1 e o algoritmo para. Logo, o número de iterações é aproximadamente lg( n) no pior caso, sendo lg(n) o piso de log2n. O número de comparações de x com elementos do vetor é o dobro disso: 2 lg(n) no pior caso. O consumo de tempo da busca binária é, portanto, proporcional a lg(n). Isso é muito melhor que o consumo de tempo da busca sequencial, pois lg transforma multiplicações em somas. Por exemplo, se cada iteração consome 1 microssegundo então uma busca em n elementos consome lg(n) microssegundos, uma busca em 2n elementos consumirá apenas lg(n)+1 microssegundos, uma busca em 8n elementos consumirá apenas lg(n)+3 microssegundos e uma busca em 1024 n elementos consumirá apenas lg(n)+10 microssegundos. Exercícios 3. Responda as seguintes perguntas sobre a função buscaBinaria descrita acima. a. Que acontece se b. Que acontece se c. Que acontece se = m-1"? d. Que acontece se "while (e <= d)" for trocado por "while (e < d)"? "while (e <= d)" for trocado por "while (e <= d+1)"? "e = m+1" for trocado por "e = m"? E se for trocado por "e "d = m-1" for trocado por "d = m" ou por "d = m+1"? 4. Suponha que v[i] = i para cada i. Execute buscaBinaria com n = 9, e com vários valores de x. Repita o exercício com n = 14 e x = 9. Repita o exercício com n = 15 e x = 9. 5. Suponha que o vetor v tem 511 elementos e que x não está no vetor. Quantas comparações a função buscaBinaria fará entre x e elementos do vetor? 6. Se preciso de t segundos para fazer uma busca binária em um vetor com n elementos, de quando tempo preciso para fazer uma busca em n² elementos? http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html Page 4 of 8 Algoritmos: busca binária (versão simplificada) 5/14/13 6:56 PM 7. Confira a validade da seguinte afirmação: quando n+1 é uma potência de 2, a expressão (e+d) é sempre divisível por 2, quaisquer que sejam v e x. 8. Execute a função buscaBinaria com n = 16. Quais os possíveis valores de m na primeira iteração? Quais os possíveis valores de m na segunda iteração? Na terceira? Na quarta? 9. Escreva uma versão da busca binária que procure x em v[0..n]. Escreva outra versão para v[1..n]. 10. A seguinte implementação da busca binária funciona corretamente? Qual o invariante dessa versão? e = -1; d = n; while (e < d-1) { m = (e + d)/2; if (v[m] == x) return m; if (v[m] < x) e = m; else d = m; } return -1; Que acontece se trocarmos "while (e < d-1)" por "while (e < d)"? Versão recursiva da busca binária Para formular uma versão recursiva da busca binária é preciso generalizar ligeiramente o problema, trocando v[0..n-1] por v[e..d]. Para estabelecer uma ponte entre a formulação original e a generalizada, vamos usar uma "função-embrulho" buscaBinaria2, que repassa o serviço para a função recursiva bb. // // // // A função abaixo recebe um número x e um vetor crescente v[0..n-1]. Ela devolve um índice m em 0..n-1 tal que v[m] == x. Se tal m não existe, devolve -1. int buscaBinaria2( int x, int n, int v[]) { return bb( x, 0, n-1, v); } // // // // A função bb recebe um número x e um vetor crescente v[e..d]. Ela devolve um índice m em e..d tal que v[m] == x. Se tal m não existe, devolve -1. int bb( int x, int e, int d, int v[]) { if (e > d) return -1; else { int m = (e + d)/2; if (v[m] == x) return m; if (v[m] < x) http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html Page 5 of 8 Algoritmos: busca binária (versão simplificada) 5/14/13 6:56 PM return bb( x, m+1, d, v); else return bb( x, e, m-1, v); } } Qual a "profundidade da recursão" da função bb? Ou seja, quantas vezes bb chama a si mesma? Resposta: cerca de log2n vezes. Exercícios 11. Critique a seguinte versão da função bb. A função recebe um número x e um vetor crescente v[e..d] tal que e ≤ d e promete devolver um índice m em e..d tal que v[m] == x (ou -1, se tal m não existe). int bb( int x, int e, int d, int v[]) { int m; if (e == d) { if (v[d] == x) return d; else return -1; } m = (e + d) / 2; if (v[m] == x) return m; if (v[m] < x) return bb( x, m+1, d, v); else return bb( x, e, m-1, v); } Mais exercícios 12. VETOR DE STRINGS. Suponha que v[0..n-1] é um vetor de strings em ordem lexicográfica. Escreva uma função que receba uma string x e devolva um índice m tal que a string x é igual à string v[m]. Se tal m não existe, a função deve devolver -1. 13. VETOR DE STRUCTS. Suponha que cada elemento do vetor v[0..n-1] é uma struct com dois campos: o nome de um aluno e o número do aluno. Suponha que o vetor está em ordem crescente de números. Escreva uma função de busca binária que receba o número de um aluno e devolva o seu nome. Se o número não está no vetor, a função deve devolver a string vazia. Exercícios: divisão e conquista http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html Page 6 of 8 Algoritmos: busca binária (versão simplificada) 5/14/13 6:56 PM O paradigma da busca binária serve de base para o conhecido "método da divisão e conquista", que pode ser usado para resolver eficientemente muitos problemas computacionais. 14. Escreva uma função que receba um vetor inteiro estritamente crescente v[0..n-1] e devolva um índice i entre 0 e n-1 tal que v[i] == i ; se tal i não existe, a função deve devolver -1. O seu algoritmo não deve fazer mais que lg(n) comparações envolvendo elementos de v. 15. Escreva uma função eficiente que receba inteiros positivos k e n e calcule k n. Quantas multiplicações sua função executa? 16. A seguinte função recursiva pretende encontrar o valor de um elemento máximo de v[e..d], supondo e ≤ d. (É claro que não estamos supondo que o vetor está ordenado.) int max( int e, int d, int v[]) { if (e == d) return v[d]; else { int m, maxe, maxd; m = (e + d)/2; maxe = max( e, m, v); maxd = max( m+1, d, v); if (maxe >= maxd) return maxe; else return maxd; } } A função está correta? Ela é mais rápida que a versão iterativa arroz-com-feijão? Quantas vezes a função chama a si mesma? URL of this site: www.ime.usp.br/~pf/algoritmos/ Last modified: Sun Jan 20 10:24:22 BRST 2013 Paulo Feofiloff IME-USP http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html Page 7 of 8 Algoritmos: busca binária (versão simplificada) http://www.ime.usp.br/~pf/algoritmos/aulas/bubi2.html 5/14/13 6:56 PM Page 8 of 8