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.