Laços de repetição

From Applied Science

"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.


É preciso entender bem a operação de atribuição e a ordem das operações de comparação para não ter dúvidas aqui


  • 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:

  1. Nenhum primo é par, com exceção do 2;
  2. Se nenhum primo é par, então nenhum primo é divisível por número par;
  3. 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.