Laços de repetição
"Computadores nunca ficam cansados e nunca reclamam" - Gracinha que o meu professor fazia nas aulas
Inicialmente, quando você aprende a utilizar laços de repetição, você ignora o desempenho do seu algoritmo. A questão do desempenho só é estudada mais a fundo em disciplinas mais avançadas, específicas para estudar algoritmos mais complicados como algoritmos de busca ou algoritmos de ordenação. Mas já é possível pensar na questão do desempenho da seguinte forma: uma repetição a mais no seu algoritmo e ele já é mais lento do que um algoritmo que faça o mesmo com uma repetição a menos.
A lógica de um laço é: algo é executado enquanto alguma condição não é satisfeita. Ou, algo é executado inúmeras vezes até que uma condição seja satisfeita.
Erros de lógica:
- A condição do laço nunca é satisfeita, o laço nunca é executado;
- A condição do laço é satisfeita e o laço é executado, mas uma vez no laço, a condição de saída nunca é satisfeita e o laço nunca para;
- A condição do laço é satisfeita, mas não quando você queria que fosse.
- Uma variação do algoritmo que testa se um número é par ou ímpar sem utilizar estruturas de seleção:
int a;
/* Enquanto o usuário não entra com um número par, continue pedindo um valor par */
while (a % 2 != 0) {
printf("O numero %d eh impar, digite outro: ", a);
scanf("%d", &a);
}
/* Se o usuário entrou com um número par, imprima isto */
printf("O numero %d eh par, saindo do programa...", a);
While. É o tipo mais simples de laço de repetição. Não há uma contagem, pois não há um número pré determinado de ocorrências de um número ímpar para o laço continuar repetindo.
Dentro do laço é preciso colocar uma nova chamada do scanf(), senão perceba que o programa não "volta atrás". Uma vez dentro do laço só é possível sair se a condição de execução do laço vier a ser falsa em algum momento. Sem scanf() no laço o programa simplesmente imprime na tela a mensagem que diz que o número é ímpar sem parar.
PS: por enquanto o mecanismo interno que faz o scanf() "pausar" o laço é omitido porque se trata de uma função.
- Um problema de contagem, calcular a soma dos primeiros n números inteiros positivos:
int num, cont = 0, soma = 0;
/* conte de zero até o número desejado */
while (cont < num) {
cont = cont + 1;
soma = soma + cont;
}
printf("A soma dos %d primeiros numeros inteiros positivos eh: %d", a, soma);
É um problema muito simples que já mostra a diferença entre fazer contas no papel e no computador. No papel a soma dos 3 primeiros inteiros é 1 + 2 + 3, são duas operações de soma. Mas no computador são duas contagens: uma que conta o número a ser somado e outra que faz a soma propriamente. A outra diferença é na memória, qualquer soma feita mentalmente ou no papel está utilizando a memória implicitamente. No computador a operação de atribuição de valor a uma variável é exposta, afinal o computador não memoriza nada sozinho. Isso explica por que as variáveis 'cont' e 'soma' começam do zero e não do um.
Qualquer confusão com relação a isto pode ser sanada simulando a execução do laço no papel.
Vejamos o mesmo laço, mas contando a partir do um e não do zero:
int num, cont = 1, soma = 0;
/* conte de um até o número desejado */
while (cont <= num) {
soma = soma + cont;
cont = cont + 1;
}
Como exercício, tente fazer a contagem em ordem decrescente.
- A mesma contagem, mas com FOR no lugar de WHILE:
int num, cont, soma = 0;
/* para cont igual a zero, conte mais um de zero até o número desejado */
for (cont = 1; cont <= num; cont++;) {
soma = soma + cont;
}
A diferença de um 'for' para um 'while' está na sintaxe, apenas isso. O laço do tipo 'for' é utilizado sempre que necessitamos de uma contagem de iterações.
Flexibilidade. Iniciar o valor do contador pode ser feito antes do laço (mas não dentro!). A condicional do laço não precisa estar relacionada com o contador. Por exemplo, existem casos em que a condicional é uma aproximação de um valor e essa aproximação acontece por um número de passos que é variável. A condicional pode até ser omitida, mas aí é preciso colocar um 'break' no laço para ter alguma condição para interromper a execução. O incremento, pela sintaxe da linguagem, é a última operação do laço. É permitido omitir a expressão de incremento do parêntesis para colocá-la em qualquer ordem no laço.
- A mesma contagem, mas DO WHILE:
/* conte de um até o número desejado */
do {
soma = soma + cont;
cont = cont + 1;
}
while (cont <= num);
A diferença entre while e do while é que no primeiro a condicional é avaliada antes de cada iteração, no segundo a condicional é avaliada depois de cada iteração. Na prática, do while não pode ser usado quando a condicional do laço não é satisfeita ao menos uma vez.
- Um segundo problema de contagem, some os n primeiros números primos:
#define SIM 1
#define NAO 0
/* variável numero é a quantidade de primos a ser somada
eh_primo indica quando um número é primo ou não
achei_mais_um conta quantos primos foram achados até o momento
soma acumula a soma de primos
primo conta números ímpares */
int numero;
int eh_primo;
int achei_mais_um = 0, soma = 0;
int primo;
int divisor, resto;
/* soma primos, desde um até n. Pule os números pares. */
for (primo = 1; achei_mais_um < numero; primo += 2) {
/* todo ímpar pode ser um primo */
eh_primo = SIM;
/* 1 é ímpar mas não é primo */
if (primo == 1)
eh_primo = NAO;
/* 3 é primo, não precisa verificar se é ou não
2 é primo mas é par, uma exceção. Conte sem verificar a divisibilidade */
else if (primo == 3) {
achei_mais_um++;
soma = 2;
}
/* se o número não é 1 e nem 3, só resta ser um ímpar primo ou ímpar não primo
teste a divisibilidade apenas com números ímpares até o limite de metade do
número. Se resto igual a zero, o número não é primo */
else {
for (divisor = 3; divisor < primo / 2; divisor += 2) {
resto = primo % divisor;
if (resto == 0) {
eh_primo = NAO;
break;
}
}
}
/* se o número é primo, conte e faça a soma */
if (eh_primo == SIM) {
achei_mais_um++;
soma = soma + primo;
}
}
O algoritmo usa a "força bruta" para fazer a soma porque não existe uma fórmula para encontrar todos os primos. Se houvesse seria possível utilizar uma fórmula como a fórmula da progressão aritmética. Porém, algumas otimizações matemáticas são possíveis:
- Nenhum primo é par, com exceção do 2;
- Se nenhum primo é par, então nenhum primo é divisível por número par;
- O laço que testa a divisibilidade de um número ímpar é quebrado no primeiro divisor ímpar encontrado. O laço também não testa todos os potenciais divisores ímpares possíveis, apenas até a metade do número;
'break' só interrompe a iteração corrente do laço no qual ele está inserido, se este laço está inserido dentro de outro, nada acontece com o laço externo.
Note que o algoritmo é na verdade uma combinação de dois algoritmos mais simples, um que faz a soma e outro que testa se um número é primo ou não. Em problemas assim é uma boa ideia abordar partes menores do problema em separado, assim a solução final é a união das soluções menores.
A variável eh_primo poderia começar com o valor negativo NAO. Mas aí o teste de divisibilidade deveria trocar o valor da variável para SIM, caso todos os divisores possíveis fossem testados e nenhum fosse divisor do número.
- Um algoritmo que soma os n primeiros números, com exceção dos múltiplos de 3 e 5:
int num, cont, soma = 0;
/* conte de um em um. Se o número for múltiplo de 3 e de 5, pule a soma. */
for (cont = 1; cont <= num; cont++) {
if (cont % 3 == 0 && cont % 5 == 0)
continue;
soma = soma + cont;
}
O uso de continue é bastante similar ao uso de 'break'. Se a condicional é satisfeita, 'continue' simplesmente faz todas as operações que estão abaixo dele não serem executadas. Em outras palavras, ele significa literalmente "continue para a próxima iteração". Este é um tipo de estrutura prática que não é muito elegante por assim dizer. Não é exigida nos algoritmos vistos numa introdução, inclusive porque sempre é possível não utilizar continue.
Cuidado! Se a operação de incremento do contador estiver após continue e não no parêntesis, uma repetição infinita é criada.
Vejamos o mesmo algoritmo sem usar continue:
int num, cont, soma = 0;
/* conte de um em um. Só some se o número não for múltiplo de 15 */
for (cont = 1; cont <= num; cont++) {
if (cont % 15 != 0)
soma = soma + cont;
}
Apenas uma pequena modificação, um número que é múltiplo de 3 e 5 ao mesmo tempo é múltiplo de 15.