• Nenhum resultado encontrado

Estruturas recursivas na arte

4.6 Eficiência e correção

Dos tópicos restantes que poderiam ser discutidos como parte da nossa introdução aos algoritmos, dois conceitos são tratados nesta seção, cobrindo problemas que vêm à mente quando alguém desenvolve seus próprios programas. O primeiro destes é a eficiência, e o outro, a correção* dos algoritmos.

?

*N. de T. Em inglês, correctness — neste contexto, a palavra correção está sendo empregada no sentido de qualidade de correto.

Eficiência de algoritmos

Embora as máquinas modernas sejam capazes de executar milhões de instruções por segundo, a eficiência continua sendo uma preocupação central no projeto de algoritmos. Geralmente a escolha entre um algoritmo eficiente e um ineficiente pode significar, para um dado problema, a diferença entre uma solução prática ou não.

Consideremos o problema de matrícula universitária, que necessita localizar e atualizar registros de estudantes. Embora a universidade tenha, de fato, cerca de 10.000 estudantes em cada semestre, seu “arquivo corrente de estudantes” contém o registro de mais de 30.000, que serão considerados estudan- tes correntes se tiverem feito matrícula em pelo menos um curso nos últimos anos, mas que não conclu- íram uma etapa inteira do curso. Podemos imaginar estes registros armazenados no computador da seção de alunos, em uma lista ordenada segundo o número de identificação dos estudantes. Para encontrar qualquer registro de estudante, o encarregado irá essencialmente procurar nesta lista, ordenada de acor- do com o número de identificação do estudante.

Foram apresentados dois algoritmos de busca para tal lista: a busca seqüencial e a binária. A questão agora é decidir se uma escolha entre estes dois algoritmos faria alguma diferença no caso do arquivo de estudantes. Consideremos inicialmente a busca seqüencial.

Dado um número de identificação de estudante, o algoritmo de busca seqüencial inicia a busca no começo da lista e compara um a um com o número desejado os elementos da lista. Se não houver infor- mação acerca do valor procurado, nada concluiremos sobre a extensão da sua busca nesta lista. Entretan- to, podemos dizer que, depois de muitas buscas, a extensão média da busca deverá ser aproximadamente a metade da extensão total da lista; algumas serão menores; outras, mais longas. Concluímos que, dentro de um intervalo de tempo, a busca seqüencial testará em média uns 15.000 registros por busca. Se para obter e conferir um número de identificação de um registro for necessário 10 milissegundos (dez milési- mos de segundo) de processamento, esta busca se completará, em média, em 150 segundos ou 2,5 minu- tos — tempo intolerável para o encarregado esperar o registro do estudante aparecer na tela. Mesmo se o tempo para recuperar e conferir cada registro fosse reduzido para apenas 1 milissegundo, a busca ainda exigiria em média 15 segundos, o que é ainda um tempo de espera longo.

A busca binária, porém, opera pela comparação do valor desejado com o elemento que se encontra no meio da lista. Se este não for o elemento desejado, então, pelo menos, o restante da busca se restrin- girá à metade da lista original. Assim, depois de testar o elemento central de uma lista de 30.000 regis- tros de estudantes, a busca binária ainda deverá verificar no máximo 15.000 registros. Após o segundo teste, permanecem no máximo 7.500, e depois da terceira tentativa, a lista em questão estará reduzida a não mais que 3.750 elementos. Continuando desta forma, verificamos que, se o registro desejado estiver na lista, será encontrado depois do teste de, no máximo, 15 dos 30.000 registros da lista. Assim, se cada tentativa for executada em 10 milissegundos, a busca de um dado registro, usando esse algoritmo, exigi- rá somente 0,15 segundos — o que significa que o acesso a um registro de estudante específico parecerá instantâneo ao encarregado. Concluímos que a escolha entre o algoritmo de busca seqüencial e o de busca binária terá um impacto significativo nessa aplicação.1

Esse exemplo mostra a importância da área da Ciência da Computação conhecida como análise de algoritmos, que inclui o estudo de recursos como o tempo e espaço de armazenamento que os algoritmos necessitam. Uma aplicação primordial desses estudos é a avaliação dos méritos relativos de algoritmos alternativos. Em nosso caso, analisamos o tempo exigido pelos algoritmos de busca seqüencial e binária para determinar qual era a melhor solução em uma aplicação específica. Em geral, essa análise é realiza- da em um contexto mais genérico, isto é, quando consideramos algoritmos para pesquisar listas, não focalizamos uma lista particular de comprimento determinado, mas tentamos identificar uma fórmula

1Para obter os benefícios do algoritmo de busca binária, os registros de estudante devem ser armazenados de maneira a permitir que os elementos do meio das sublistas possam ser recuperados sem muito trabalho. Estudaremos como fazer isso nos Capítulos 7 e 8.

que indique o desempenho do algoritmo em listas de comprimento arbitrário. Esse tipo de análise geral- mente envolve a identificação do melhor caso, do pior e do médio.

Nossa análise prévia focalizou o desempenho médio do algoritmo de busca seqüencial e o pior caso de algoritmo de busca binária. Embora tenhamos nos concentrado em uma lista de comprimento determinado, não é difícil generalizar nosso raciocínio para listas de comprimento arbitrário. Em particu- lar, quando aplicado a uma lista com n elementos, o algoritmo de busca seqüencial irá interrogar em média n/2 elementos, enquanto o de busca binária consultará no máximo lg n elementos, na pior das hipóteses. (lg n representa o logaritmo de n na base dois.)

Vamos analisar agora o algoritmo de ordenação por inserção (resumido na Figura 4.11) de uma maneira similar. Lembre-se de que esse algoritmo envolve a seleção de um elemento chamado pivô, a sua comparação com aqueles que o precedem até o seu lugar adequado ser encontrado, e então a sua inser- ção. Uma vez que a atividade de comparar dois nomes predomina no algoritmo, nossa abordagem será a de encontrar o número de comparações feitas quando se ordena uma lista de comprimento n.

O algoritmo inicia selecionando o segundo elemento da lista como pivô e então progride, tomando os elementos sucessivos como pivôs, até que seja alcançado o fim da lista. No melhor caso, cada pivô já está em seu lugar e, assim, necessita ser comparado com apenas um nome para que isso possa ser cons- tatado. Portanto, no melhor caso, a aplicação da ordenação por inserção a uma lista de n elementos requer n – 1 comparações. (O segundo elemento é comparado com um nome, o terceiro, com um nome, e assim por diante.)

Entretanto, o pior caso é aquele no qual cada pivô deve ser comparado com todos os elementos precedentes para que seu lugar adequado possa ser encontrado. Isso ocorrerá se a lista original estiver na ordem inversa à desejada. Nesse caso, o primeiro pivô (segundo elemento da lista) é comparado com um nome, o segundo pivô (terceiro elemento da lista), com dois nomes e assim por diante (Figura 4.18). Portanto, o número total de comparações ao ordenar uma lista com n elementos é 1 + 2 + 3+... + (n – 1), que equivale a n(n – 1)/2 ou (1/2)(n2 – n). Por exemplo, se a lista contiver 10 elementos, o pior caso

para o algoritmo de ordenação por inserção exigirá 45 comparações.

No caso médio da ordenação por inserção, devemos esperar que cada pivô seja comparado com a metade dos elementos precedentes. Isso resulta na metade das comparações realizadas no pior caso, um total de (1/4) (n2 – n) comparações para ordenar uma lista com n nomes. Se, por exemplo, usarmos a

ordenação por inserção para ordenar várias listas de comprimento 10, deveremos esperar que o número médio de comparações seja 22,5.

O significado desse resultado é que o número de comparações feitas durante a execução do algo- ritmo de ordenação por inserção dá uma aproximação do tempo necessário para executar o algoritmo. Usando essa aproximação, a Figura 4.19 mostra um gráfico que indica como o tempo exigido para execu- tar o algoritmo de ordenação por inserção cresce em função do comprimento da lista. Esse gráfico se baseia em nossa análise do pior caso do algoritmo, na qual concluímos que ordenar uma lista de compri- mento n exige no máximo (1/2)(n2 – n) comparações entre seus elementos. No gráfico, marcamos vários

comprimentos de lista e indicamos o tempo necessário em cada caso. Note que à medida que os compri- mentos crescem com incrementos uniformes, o tempo exigido para ordenar a lista cresce com incremen-

Figura 4.18 Aplicação da ordenação por inserção no pior caso.

tos cada vez maiores. Assim, o algoritmo vai se tornando me- nos eficiente à medida que o ta- manho da lista aumenta.

Vamos aplicar uma aná- lise similar ao algoritmo de busca binária. Lembre-se de que concluímos que pesquisar uma lista com n elementos usando esse algoritmo envol- ve a consulta de, no máximo, lg n elementos, o que, mais uma vez, dá uma aproximação do tempo necessário para exe- cutar o algoritmo em listas de vários comprimentos. A Figu- ra 4.20 mostra um gráfico ba- seado nessa análise, no qual novamente marcamos vários comprimentos de lista igual- mente espaçados e identifica- mos o tempo gasto pelo algo- ritmo em cada caso. Note que o tempo aumenta com incre-

mentos cada vez menores, isto é, o algoritmo de busca binária torna-se mais eficiente à medida que o tamanho da lista aumenta.

A característica de distinção entre as Figuras 4.19 e 4.20 é a forma geral das curvas envolvidas. É a forma geral de um gráfico, em vez das específicas, que revela o desempenho de um algoritmo à medida que seus dados de entrada cres-

cem. Observe que a forma ge- ral de um gráfico é determina- da pelo tipo de expressão que está sendo desenhada em vez da expressão específica — to- das expressões lineares produ- zem uma linha reta; todas as quadráticas produzem uma pa- rábola; todas as logarítmicas produzem a forma logarítmica mostrada na Figura 4.20. É cos- tume identificar uma curva com a expressão mais simples que produz a sua forma. Em parti- cular, identificamos a forma parabólica com a expressão n2

e a logarítmica com a expres- são lg n.

Vimos que a forma do gráfico obtido na comparação do tempo exigido por um al- goritmo na realização de sua

Figura 4.19 Gráfico de análise do pior caso do algoritmo de ordenação por inserção.

Figura 4.20 Gráfico de análise do pior caso do algoritmo de busca binária.

tarefa com o tamanho dos dados de entrada reflete as características de eficiência do algoritmo. As- sim, é comum classificar os algoritmos de acordo com a forma desses gráficos — normalmente basea- dos em análise de pior caso. A notação usada para identificar essas classes é chamada “notação teta”. Todos os algoritmos cujo gráfico tem a forma de uma parábola, como o de ordenação por inserção, são incluídos na classe representada por Θ(n2) (leia-se “teta de n ao quadrado”); todos os algoritmos cujo

gráfico tem a forma de expressão logarítmica, como o da busca binária, são incluídos na classe identi- ficada por Θ(lg n) (leia-se “teta de log n”). Sabendo-se a classe à qual um particular algoritmo perten- ce, podemos prever o seu desempenho e compará-lo com outros algoritmos para resolver o mesmo problema. Dois algoritmos em Θ(n2) terão tempos similares quando o tamanho de suas entradas cres-

cer, enquanto a exigência de tempo de um algoritmo em Θ(lg n) não se expandirá tão rapidamente quanto a de um outro em Θ(n2).

Verificação de software

Relembramos que a quarta fase da análise de Polya sobre a resolução de problemas (Seção 4.3) trata da avaliação da solução quanto à sua precisão e ao seu potencial como ferramenta para a resolução de outros problemas. O significado da primeira parte desta fase pode ser captado no seguinte exemplo:

Um viajante, com uma corrente de ouro formada por uma cadeia de sete argolas, deve ficar por sete noites em um hotel isolado. O aluguel por uma noite equivale a uma argola da corrente. Qual o número mínimo de argolas que devem ser cortadas de modo que o viajante possa pagar ao hoteleiro uma argola a cada manhã, sem antecipar o pagamento pela hospedagem?

Primeiro, percebemos que nem toda argola da corrente deve ser cortada. Se cortarmos somente a segunda argola, poderíamos liberar a primeira e a segunda das outras cinco. Seguindo este raciocí- nio, a solução seria cortar a segunda, a quarta e a sexta argolas da corrente e teríamos um processo que solta todas as argolas cortando somente três (Figura 4.21). Além disso, qualquer número menor de cortes deixaria duas argolas conectadas, e assim concluímos que a resposta correta para o nosso problema é três.

Entretanto, após reconsiderarmos o problema, observamos que quando cortamos somente a terceira argola da corrente, obtemos três pedaços de correntes de comprimentos iguais a um, dois e quatro (Figura 4.22). Com estes pedaços, podemos proceder da seguin- te forma:

Na primeira manhã, entregar ao hoteleiro a única argola solta.

Na segunda, receber de volta esta argola e entregar ao hoteleiro a corrente de duas argolas.

Na terceira, entregar novamente a argola sol- ta ao hoteleiro.

Na quarta, receber de volta as correntes que ficaram com o hoteleiro e lhe entregar a corrente de quatro argolas.

Na quinta, entregar novamente ao hoteleiro a argola solta.

Na sexta, receber esta argola de volta e entre- gar a corrente de duas argolas.

Na sétima, entregar novamente a argola solta.

Além da verificação de software