Previous Up Next
Programação de Computadores em C

Capítulo 4  Recursão e Iteração

Os conceitos de recursão e iteração constituem, juntamente com as noções de composição seqüencial e seleção, as ferramentas fundamentais para construção de algoritmos e programas, a partir de um conjunto apropriado de operações ou comandos básicos. Esses dois conceitos são introduzidos neste capítulo, por meio de uma série de exemplos ilustrativos, de construção de programas para cálculo de operações aritméticas simples, tais como:

A solução de qualquer problema que envolva a realização de uma ou mais operações repetidas vezes pode ser expressa, no paradigma de programação imperativo, por meio de um comando de repetição (também chamado de comando iterativo, ou comando de iteração), ou usando funções com definições recursivas.

Definições recursivas de funções são baseadas na mesma idéia subjacente a um princípio de prova fundamental em matemática — o princípio da indução. A idéia é a de que a solução de um problema pode ser expressa da seguinte forma: primeiramente, definimos a solução desse problema para casos básicos; em seguida, definimos como resolver o problema para os demais casos, em termos da solução para casos mais simples que o original.

4.1  Multiplicação e Exponenciação

Considere por exemplo o problema de definir a multiplicação de dois números inteiros m e n, sendo n>0, em termos da operação de adição. O caso mais simples dessa operação é aquele no qual n=0: nesse caso, o resultado da operação é igual a 0, independentemente do valor de m. De acordo com a idéia exposta acima, precisamos agora pensar em como podemos expressar a solução desse problema no caso em que n>0, supondo que sabemos determinar sua solução para casos mais simples, que se aproximam do caso básico. Neste caso, podemos expressar o resultado de m × n em termos do resultado da operação, mais simples m × (n−1); ou seja, podemos definir m × n como sendo igual a m + (m × (n−1)), para n>0.

Ou seja, a operação de multiplicação pode então ser definida indutivamente pelas seguintes equações:

m × n = 

0  se  n=0,  senão  m + 
m × (n−1)


Uma maneira alternativa de pensar na solução desse problema seria pensar na operação de multiplicação m * n como a repetição, n vezes, da operação de adicionar m, isto é:

     m × n = (m + … + m)n vezes 

Raciocínio análogo pode ser usado para expressar a operação de exponenciação, com expoente inteiro não-negativo, em termos da operação de multiplicação.

O programa a seguir apresenta duas definições alternativas, na linguagem C, para computar o resultado da multiplicação e da exponenciação de dois números, dados como argumentos dessas operações. As funções mult e exp são definidas usando-se um comando de repetição. As funções multr e expr são definidas usando-se chamadas à própria função que está sendo definida, razão pela qual essas definições são chamadas de recursivas.

Usamos a letra “r” adicionada como sufixo ao nome de uma função para indicar que se trata de uma versão recursiva da definição de uma função.

int mult (int m, int n) { int r=0, i; for (i=1; i<=n; i++) r += m; return r; } int multr (int m, int n) { if (n==0) return 0; else return (m + multr (m, n-1)); } int exp (int m, int n) { int r=1, i; for (i=1; i<=n; i++) r*=m; return r; } int expr (int m, int n) { if (n==0) return 1; else return (m * expr(m, n-1)); }

Como vimos na Seção 2.4, a execução de um comando de repetição consiste na execução do corpo desse comando repetidas vezes, até que a condição associada a esse comando se torne falsa (caso isso nunca ocorra, a repetição prossegue indefinidamente). Para entender melhor o significado desse comando e, portanto, o comportamento das funções mult e exp, consideramos seguir a execução de uma chamada à função mult — por exemplo, mult(3,2).

Note que essas definições são usadas apenas com fins didáticos, para explicar inicialmente definições recursivas e comandos de repetição. Essas definições não têm utilidade prática pois existe, no caso da multiplicação, o operador predefinido (*) e, no caso da exponenciação, a função math.pow (cf. seção 3.9).

A execução de uma chamada de função é iniciada, como vimos na Seção 2.5, com a avaliação dos argumentos a serem passados à função: no exemplo acima, essa avaliação fornece os valores representados pelos literais 3 e 2, do tipo int. Esses valores são atribuídos aos parâmetros m e n, respectivamente, e o corpo da função mult é então executado.

Depois da execução do corpo da função mult, os parâmetros, assim como, possivelmente, variáveis declaradas internamente no corpo da função, deixam de existir, isto é, são desalocados. Ou seja, o tempo de vida de parâmetros e variáveis declaradas localmente é determinado pelo bloco correspondente à função.

A execução do corpo da função mult é iniciada com a criação da variável r, à qual é atribuído inicialmente o valor 0. Em seguida, o comando for é executado.

O comando for é um comando de repetição (assim como o comando while, introduzido na Seção 2.4). A execução do comando for usado no corpo da função mult consiste simplesmente em adicionar (o valor contido em) m ao valor de r, a cada iteração, e armazenar o resultado dessa adição em r. O número de iterações executadas é igual a n. Por último, o valor armazenado em r após a execução do comando for é retornado, por meio da execução do comando return r;. Consideramos, a seguir, o comando for mais detalhadamente.

O cabeçalho de um comando for — no exemplo, a parte entre parênteses (int i=1; i<=ni++) — é constituído de três partes (ou cláusulas), descritas a seguir:

Note a ordem na execução de cada iteração em um comando for: teste de terminação, corpo, atualização.

É instrutivo acompanhar a modificação do valor armazenado em cada variável durante a execução da chamada mult(3,2). Em outras palavras, acompanhar, em cada passo da execução, o estado da computação: o estado da computação é uma função que associa um valor a cada variável.

A Tabela 4.1 ilustra os passos da execução da chamada mult(3,2), registrando, a cada passo, o estado da computação. O resultado fornecido pela avaliação de expressões é também mostrado nessa tabela. Note que o resultado fornecido pela cláusula de atualização de um comando for é sempre descartado. Essa expressão é avaliada apenas com o propósito de atualizar o valor de uma ou mais variáveis (ou seja, é uma expressão com efeito colateral).


Tabela 4.1: Passos na execução de mult(3,2)
Comando/
Expressão
Resultado
(expressão)
Estado
(após execução/avaliação)
mult(3,2)m ↦ 3, n ↦ 2
int r = 0 m ↦ 3, n ↦ 2, r ↦ 0
i = 1 m ↦ 3, n ↦ 2, r ↦ 0, i ↦ 1
i <= nverdadeirom ↦ 3, n ↦ 2, r ↦ 0, i ↦ 1
r += m3m ↦ 3, n ↦ 2, r ↦ 3, i ↦ 1
i++2m ↦ 3, n ↦ 2, r ↦ 3, i ↦ 2
i <= nverdadeirom ↦ 3, n ↦ 2, r ↦ 3, i ↦ 2
r += m6m ↦ 3, n ↦ 2, r ↦ 6, i ↦ 2
i++3m ↦ 3, n ↦ 2, r ↦ 6, i ↦ 3
i <= nfalsom ↦ 3, n ↦ 2, r ↦ 6, i ↦ 3
for m ↦ 3, n ↦ 2, r ↦ 6, i ↦ 3
return r  
mult(3,2)6 


A variável i é usada no comando for como um contador de iterações.

Considere agora a função multr definida acima. A sua definição espelha diretamente a definição indutiva da operação de multiplicação, em termos da adição, apresentada anteriormente. Ou seja, multiplicar m por n (onde n é um inteiro não-negativo) fornece:

Vejamos agora, mais detalhadamente, a execução de uma chamada multr(3,2). Cada chamada da função multr cria novas variáveis, de nome m e n. Existem, portanto, várias variáveis com nomes (m e n), devido às chamadas recursivas. Nesse caso, o uso do nome refere-se à variável local ao corpo da função que está sendo executado. As execuções das chamadas de funções são feitas, dessa forma, em uma estrutura de pilha. Chamamos, genericamente, de estrutura de pilha uma estrutura na qual a inserção (ou alocação) e a retirada (ou liberação) de elementos é feita de maneira que o último elemento inserido é o primeiro a ser retirado.

Como a execução de chamadas de funções é feita na forma de uma estrutura de pilha, em cada instante da execução de um programa, o último conjunto de variáveis alocados na pilha corresponde às variáveis e parâmetros da última função chamada. No penúltimo espaço são alocadas as variáveis e parâmetros da penúltima função chamada, e assim por diante. Cada espaço de variáveis e parâmetros alocado para uma função é chamado de registro de ativação dessa função.

O registro de ativação de cada função é desalocado (isto é, a área de memória correspondente na pilha é liberada para uso por outra chamada de função) no instante em que a execução da função termina. Como o término da execução da última função chamada precede o término da execução da penúltima, e assim por diante, o processo de alocação e liberação de registros de ativação de chamadas de funções se dá como em uma estrutura de pilha.

As variáveis que podem ser usadas no corpo de uma função são apenas os parâmetros dessa função e as variáveis internas aí declaradas (chamadas de variáveis locais da função), além das variáveis externas, declaradas no mesmo nível da definição da função, chamadas de variáveis globais.

Variáveis declaradas internamente a uma função são criadas cada vez que uma chamada a essa função é executada, enquanto variáveis globais são criadas apenas uma vez, quando a execução do programa é iniciada ou anteriormente, durante a carga do código do programa na memória.

No momento da criação de uma variável local a uma função, se não foi especificado explicitamente um valor inicial para essa variável, o valor nela armazenado será indeterminado, isto é, será o valor já existente nos bytes alocados para essa variável na pilha de chamadas de funções.

Na Figura 4.2, representamos a estrutura de pilha criada pelas chamadas à função multr. Os nomes m e n referem-se, durante a execução, às variáveis mais ao topo dessa estrutura. Uma chamada recursiva que vai começar a ser executada é circundada por uma caixa. Nesse caso, o resultado da expressão, ainda não conhecido, é indicado por “”.


Tabela 4.2: Passos na execução de multr(3,2)
Comando/
Expressão
Resultado
(expressão)
Estado
(após execução/avaliação)
multr (3,2) 
m 3
n 2
n == 0falso
m 3
n 2
return m+multr(m,n-1)
m 3   m 3
n 2   n 1
n == 0falso
m 3   m 3
n 2   n 1
return m+multr(m,n-1)
m 3   m 3   m 3
n 2   n 1   n 0
n == 0verdadeiro
m 3   m 3    m 3
n 2   n 1    n 0
return 0 
m 3   m 3
n 2   n 1
return m + 0 
m 3
n 2
return m + 3  
multr (3,2)6 


Uma chamada de função corresponde então nos seguintes passos, nesta ordem:

  1. Avaliação das expressões especificadas na chamada da função.

    Cada expressão usada em uma chamada em computação, correspondente a cada parâmetro, é chamada de parâmetro real. O resultado obtido pela avaliação de cada parâmetro real é chamado de argumento.

    Por exemplo, na chamada mult(3+1,2), 3+1 e 2 são parâmetros reais. Os argumentos são 4 e 2, obtidos pela avaliação das expressões 3+1 e 2, respectivamente.

  2. Criação de registro de ativação da função, contendo espaço para os parâmetros da função (e possivelmente variáveis declaradas localmente).

    No nosso exemplo, é criado registro de ativação contendo espaço para os parâmetros m e n.

  3. Atribuição dos argumentos aos parâmetros correspondentes.

    No nosso exemplo mult(3+1,2), o argumento 4 (obtido pela avaliação do parâmetro real 3+1) é atribuído ao parâmetro m.

    Essa atribuição do argumento ao parâmetro é chamada em computação de passagem de parâmetro, no caso uma passagem de parâmetro por valor. O significado disso é o já explicado: o parâmetro real é avaliado, resultado em um valor, chamado de argumento, e copiado para o parâmetro. O parâmetro é também chamado de parâmetro formal.

  4. Execução do corpo da função.
  5. Liberação do espaço de memória alocado para o registro de ativação da função.

    É preciso, é claro, que o resultado da função fique disponível para ser usado, ou seja armazenado em uma variável que exista depois que o espaço de memória alocado para o registro de ativação tiver sido liberado, após a chamada à função.

As definições de exp e expr seguem o mesmo padrão de computação das funções mult e multr, respectivamente. A definição de expr é baseada na seguinte definição indutiva da exponenciação:

mn = (1  se  m=0,  senão  m × mn−1)

Eficiência

Uma característica importante da implementação de funções, e de algoritmos em geral, é a sua eficiência. A definição alternativa da função de exponenciação apresentada na Figura 4.1 ilustra bem como implementações de uma mesma função, baseadas em algoritmos distintos, podem ter eficiência diferente. Essa nova definição é uma implementação de mn baseada na seguinte definição indutiva:

      m0 = 1
      mn = (mn/2)2 se n é par (n>0)
      mn = m × mn−1 se n é ímpar (n>0)

int exp2 (int m, int n) { if (n == 0) return 1; else if (n % 2 == 0) { // n é par int x = exp2(m, n/2); return x*x; } else return m * exp2(m, n-1); }
Figura 4.1: Implementação mais eficiente da operação de exponenciação


A avaliação de exp2(mn) produz o mesmo resultado que a avaliação de exp(mn) e de expr(mn), mas de maneira mais eficiente — em termos do número de operações necessárias, e portanto em termos do tempo de execução. Para perceber isso, observe que a avaliação de exp2(mn) é baseada em chamadas recursivas que dividem o valor de n por 2 a cada chamada; a avaliação de exp(mn), por outro lado, diminui o valor de n de 1 a cada iteração (assim como expr(mn) diminui o valor de n de 1 a cada chamada recursiva).

Por exemplo, as chamadas recursivas que ocorrem durante a avaliação da expressão exp2(2,20) são, sucessivamente, as seguintes:

exp2(2,20) exp2(2,10) exp2(2,5) exp2(2,4) exp2(2,2) exp2(2,1) exp2(2,0)

A diferença em eficiência se torna mais significativa quando o expoente n é maior. São realizadas da ordem de log2(n) chamadas recursivas durante a execução de exp2(mn) — uma vez que n é em média dividido por 2 em chamadas recursivas —, ao passo que a execução de exp(mn) requer n iterações.

Um exercício interessante é aplicar a mesma técnica usada na definição de exp2 (ou seja, usar divisão por 2 caso o segundo operando seja par) para obter uma definição mais eficiente para a multiplicação (procure resolver esse exercício antes de ver sua solução, no Exercício Resolvido 2).

Questões sobre eficiência de algoritmos são importantes em computação, envolvendo aspectos relativos ao tempo de execução e ao consumo de memória (referidos abreviadamente como aspectos de tempo e espaço). A análise da eficiência de algoritmos é abordada superficialmente ao longo deste texto, sendo um estudo mais detalhado desse tema deixado para cursos posteriores.

4.2  Fatorial

Nesta seção, exploramos um pouco mais sobre recursão e iteração, por meio do exemplo clássico do cálculo do fatorial de um número inteiro não-negativo n, usualmente definido como:

n! = n × (n−1) × (n−2) × … 3 × 2 × 1

Duas formas comuns para implementação dessa função são mostradas a seguir:

int fat (int n) { int f=1, i; for (i=1; i<=n; i++) f *= i; return f; } int fatr (int n) { if (n == 0) return 1; else return n * fatr(n-1); }

A função fatr espelha diretamente a seguinte definição indutiva, que especifica precisamente o significado dos três pontos () na definição de n! dada acima:

        n! = 1 se n = 0
        n! = n × (n−1)! em caso contrário

A versão iterativa, fat, adota a regra de calcular o fatorial de n usando um contador (i), que varia de 1 a n, e uma variável (f), que contém o produto 1 × 2 × … i, obtido multiplicando o valor de i por f a cada iteração. Mudando o valor do contador e o valor do produto a cada iteração (para valores de i que variam de 1 a n), obtemos o resultado desejado na variável f (ou seja, n!), ao final da repetição. Verifique que, no caso da execução de uma chamada fat(0), nenhuma iteração do comando for é executada, e o resultado retornado pela função é, corretamente, igual a 1 (valor inicialmente atribuído à variável f).

O exemplo a seguir mostra como pode ser implementada uma versão recursiva do processo iterativo para cálculo do fatorial. Nesse caso, o contador de repetições (i) e a variável (f), que contém o produto 1 × 2 × … i, são passados como argumentos da função definida recursivamente, sendo atualizados a cada chamada recursiva.

int fatIter (int n, int i, int f) { return (i > n ? f : fatIter(n, i+1, f*i); } int fatr1 (int n) { return fatIter(n,1,1); }

4.3  Obtendo Valores com Processos Iterativos

Outros exemplos de problemas cuja solução requer o uso de recursão ou iteração são abordados a seguir. De modo geral, na solução de tais problemas, o valor calculado em uma iteração ou chamada recursiva de uma função é obtido pela aplicação de funções a valores obtidos na iteração ou chamada recursiva anterior.

O exemplo mais simples é o cálculo do somatório de termos de uma série aritmética. Por exemplo, considere o cálculo do somatório dos termos da série aritmética de passo 1 para um dado n, usualmente denotado por

n
i=1
 i 

implementada a seguir pela função pa1:

int pa1 (int n) { int s = 0, i; for (i=1; i<=n; i++) s += i; return s; }

Assim como para o cálculo do fatorial, duas versões recursivas, que usam processos de cálculo diferentes, podem ser implementadas. Essas definições são mostradas a seguir. A primeira (pa1r) espelha diretamente a definição indutiva de

n
i=1
 i

e a segunda (pa1rIter) espelha o processo iterativo, de maneira análoga à usada na definição de fatIter.

int pa1r (int n) { return (n==0 ? 0 : n + pa1r(n-1)); } int pa1Iter (int n, int i, int s) { return (i > n ? s : pa1Iter(n, i+1, s+i)); } int pa1rIter (int n) { return pa1Iter(n,1,0); }

É simples modificar essas definições para o cálculo de somatórios dos termos de séries aritméticas com passo diferente de 1. Essas modificações são deixadas como exercício para o leitor.

Nenhuma dessas implementações constitui a maneira mais eficiente para calcular a soma de termos de uma série aritmética, pois tal valor pode ser calculado diretamente, usando a fórmula:

n
i=1
 i = 
n(n+1)
2
 

O leitor com interesse em matemática pode procurar deduzir essas fórmulas. Conta a história que a fórmula acima foi deduzida e usada por Gauss quando ainda garoto, em uma ocasião em que um professor de matemática pediu-lhe, como punição, que calculasse a soma dos 1000 primeiros números naturais. Deduzindo a fórmula em pouco tempo, Gauss respondeu a questão prontamente. De fato, é possível deduzir a fórmula rapidamente, mesmo sem uso de papel e lápis, depois de perceber que a soma

  1+2++(n−1)+n 
   + n+(n−1)++2+1  
 

fornece o mesmo resultado que somar n termos iguais a n+1. A dedução da fórmula

n
i=0
 xi = 
xn+1 − 1
x − 1
 

pode ser feita com artifício semelhante — i.e. multiplicando (x0 + … + xn) por (x − 1).

Essas definições servem, no entanto, para a ilustrar a implementação de somatórios semelhantes, como nos exemplos a seguir.

Considere a implementação de uma função para cálculo de:

n
i=0
 xi 

Também nesse caso, podemos usar um comando de repetição, como na implementação de pa1, sendo adequado agora usar uma variável adicional para guardar a parcela obtida em cada iteração. Essa variável é chamada parc na implementação apresentada a seguir, que calcula o somatório das parcelas da série geométrica acima, para um dado x e um dado n:

int pg (int n, int x) { int s = 1, parc = x, i; for (i=1; i<=n; i++) { s += parc; parc *= x; } return s; }

O uso de parc em pg evita que seja necessário calcular xi (ou seja, evita que uma operação de exponenciação tenha que ser efetuada) a cada iteração. Em vez disso, a parcela da i-ésima iteração (contida em parc), igual a xi−1, é multiplicada por x para obtenção da parcela da iteração seguinte.

Para a implementação de um somatório, devemos, portanto, decidir se cada parcela a ser somada vai ser obtida a partir da parcela anterior (e nesse caso devemos declarar uma variável, como a variável parc, usada para armazenar o valor calculado para a parcela em cada iteração), ou se cada parcela vai ser obtida por meio do próprio contador de iterações. Como exemplo desse segundo caso, considere a implementação do seguinte somatório:

n
i=1
 
1
i
 

A seqüência de números 1, 1/2, 1/3, …, 1/n, … é denominada série harmônica.

A parcela a ser somada a cada iteração, 1/i, é obtida nesse caso a partir de i, e não da parcela “anterior”, 1/(i-1):

float somaSerieHarmonica (int n) { float s = 0.0f, i; for (i=1; i<=n; i++) s += 1/(float)i; return s; }

Na maioria das vezes, uma parcela pode ser calculada de maneira mais fácil e eficiente a partir da parcela calculada anteriormente. Em vários casos, esse cálculo não usa a própria parcela anterior, mas valores usados no cálculo dessa parcela. Por exemplo, considere o seguinte somatório, para cálculo aproximado do valor de π:

π = 4 * (1 − 
1
3
 + 
1
5
 − 
1
7
 + …) 

Nesse caso, devemos “guardar” não o valor mas o sinal da parcela anterior (para invertê-lo) e o denominador usado para cálculo dessa parcela (ao qual devemos somar 2 a cada iteração):

float piAprox (int n) { float s = 0.0f, denom = 1.0f; int sinal = 1, i; for (i=1; i<=n; i++) { s += sinal/denom; sinal = -sinal; denom += 2; } return 4 * s; }

Em nosso último exemplo, vamos definir uma função para calcular um valor aproximado para ex, para um dado x, usando a fórmula:

ex = 1 + (x1 / 1!) + (x2 / 2!) + … 

Para calcular a parcela a ser somada em cada iteração i do comando de repetição, a implementação usa o denominador i! e o numerador xi, calculado na parcela anterior. Optamos pelo uso de um comando while (em vez do for), para ilustração:

float eExp (float x, int n) { float s = 1.0f; int i=1; float numer = x; int denom = 1; while (i<=n) { s += numer/denom; i++; numer *= x; denom *= i; } return s; }

A decisão entre usar um comando for ou um comando while em C é, na maioria das vezes, uma questão de estética ou de gosto. Existe uma diferença quando um comando continue é usado internamente a um comando for ou um comando while (veja Exercício Resolvido 10).

Note que os dois comandos a seguir são equivalentes, se c não contiver comando de declaração de variável, que é possível somente a partir da versão C-99 da linguagem, nem um comando continue (veja Exercício Resolvido 10; no o comando c2 é executado após o comando continue no caso do comando for, mas não no caso do while):

Note, contudo, que a versão C-99 permite declarações locais no comando de inicialização (c1) do comando for, e nesse caso o escopo da declaração é diferente nos dois casos acima. A variável introduzida na declaração é visível após c1 no caso do comando while, até o final do bloco em que o comando while ocorre, mas no caso do comando for seu escopo é apenas o comando for, não podendo ser usada em comandos seguintes ao comando for.

Outro comando de repetição que pode ser usado em programas C é o comando do-while. Esse comando tem a forma:

do c; while (b);

onde $c$ é um comando e $b$ é uma expressão que denota um valor falso ou verdadeiro. Esse comando é bastante semelhante ao comando while. No caso do comando do-while, no entanto, o comando $c$ é executado uma vez, antes do teste de continuação ($b$). Por exemplo, considere o seguinte trecho de programa, que imprime os inteiros de 1 a 10, no dispositivo de saída padrão:

int i = 1; do { printf("%d ", i); i++; } while (i <= 10);

4.3.1  Não-terminação

Pode ocorrer, em princípio, que a avaliação de certas expressões que contêm símbolos definidos recursivamente, assim como a execução de certos comandos de repetição, não termine. Considere, como um exemplo extremamente simples, a seguinte definição:

int infinito() { return infinito() + 1; }

Essa declaração especifica que a função infinito retorna um valor do tipo int. Qual seria esse valor? A avaliação de uma chamada à função infinito nunca termina, pois envolve, sempre, uma nova chamada a essa função. O valor representado por uma chamada a essa função não é, portanto, nenhum valor inteiro. Em computação, qualquer tipo, predefinido ou declarado em um programa, em geral inclui um valor especial, que constitui um valor indefinido do tipo em questão e que representa expressões desse tipo cuja avaliação nunca termina ou provoca a ocorrência de um erro (como, por exemplo, uma divisão por zero).

Muitas vezes, um programa não termina devido à aplicação de argumentos a funções cujo domínio não engloba todos os valores do tipo da função. Essas funções são comumente chamadas, em computação, de parciais. Por exemplo, a definição de fat, dada anteriormente, especifica que essa função recebe como argumento um número inteiro e retorna também um número inteiro. Mas note que, para qualquer n < 0, a avaliação de fat (n) não termina. A definição de fat poderia, é claro, ser modificada de modo a indicar a ocorrência de um erro quando o argumento é negativo.

Considere agora a seguinte definição de uma função — const1 — que retorna sempre 1, para qualquer argumento:

int const1 (int x) { return 1; }

A avaliação da expressão:

const1 (infinito())

nunca termina, apesar de const1 retornar sempre o mesmo valor (1), para qualquer argumento (o qual, nesse caso, não é usado no corpo da função) cuja avaliação termina. Isso ocorre porque, em C, assim como na grande maioria das linguagens de programação, a avaliação dos argumentos de uma função é feita antes do início da execução do corpo da função.

4.4  Exercícios Resolvidos

  1. Defina recursivamente o significado do comando while.

    Solução: Esse comando pode ser definido recursivamente pela seguinte equação:

    while (b) c = if (b) { c; while (b) c }

    Obs.: A notação = é usada acima para expressar uma equação matemática.

  2. Defina uma função mult2 que use a mesma técnica empregada na definição de exp2. Ou seja, use divisão por 2 caso o segundo operando seja par, para obter uma implementação da operação de multiplicação mais eficiente do que a apresentada em multr.

    Exemplos do uso desse algoritmo são encontrados em um dos documentos matemáticos mais antigos que se conhece, escrito por um escrivão egípcio (A’h-mose), por volta de 1700 a.C.

    Solução:

    static int mult2 (int m, int n) { if (n == 0) return 0; else if (n % 2 == 0) { // n é par int x = mult2 (m, n/2); return x + x; } else return m + mult2 (m, n-1); }
  3. Dê uma definição recursiva para uma função pgr que espelhe o processo iterativo de pg. Em outras palavras, defina recursivamente uma função que, dados um número inteiro x e um número inteiro não-negativo n, retorne o valor do somatório:
    n
    i=0
     xi 
    obtendo cada termo do somatório a partir do termo anterior (pela multiplicação desse termo anterior por x) e passando como argumento, em cada chamada recursiva, o termo e o somatório obtidos anteriormente.

    Escreva um programa que leia repetidamente pares de valores inteiros x e n, até que o valor de n seja zero ou negativo, e imprima o valor do somatório ∑i=0n xi, usando a função pgr.

    Solução:

    int pgIter (int x, int n, int i, int s, int t) { if (i > n) return t; else { int sx = s*x; return pgIter(x, n, i+1, sx, t+sx); } } int pgr (int x, int n) { return pgIter (x,n,1,1,1); } int main() { int x, n; while (1) { scanf("%d%d", &x, &n); if (n <= 0) break; printf("%d", pgr(x, n)); } }
  4. Escreva uma definição para função exp2, definida na Figura 4.1, usando um comando de repetição, em vez de recursão.

    Solução:

    int exp2 (int m, int n) { int r = 1; while (n != 0) { if (n % 2 == 0) { n = n / 2; m = m * m; } else { r = r * m; n = n - 1; } return r; }

    A definição recursiva apresentada na Figura 4.1 é mais simples, pois espelha diretamente a definição indutiva de exp2, dada anteriormente. A definição acima usa um esquema não-trivial (não diretamente ligado à definição da função) para atualização do valor de variáveis no corpo do comando de repetição.

  5. Defina uma função para calcular o máximo divisor comum de dois números inteiros positivos.

    Solução: O algoritmo de Euclides é um algoritmo clássico e engenhoso para cálculo do máximo divisor comum de dois números inteiros positivos:

    mdc(a,b) = 
    a se b=0,  senão   mdc(ba % b)

    O operador % é usado acima como em C, ou seja, a%b representa o resto da divisão inteira de a por b (a e b são números inteiros). O algoritmo original de Euclides (escrito no famoso livro Elementos, por volta do ano 3 a.C.) usava mdc(b, ab) em vez de mdc(b, a % b), na definição acima, mas o uso da divisão torna o algoritmo mais eficiente.

    Note que esse algoritmo funciona se ab ou em caso contrário. Se a < b, a chamada recursiva simplesmente troca a por b (por exemplo, mdc(20,30) é o mesmo que mdc(30,20)).

    Em C, podemos escrever então:

    int mdc (int a, int b) { return (b == 0 ? a : mdc(b, a % b); }

    Podemos também escrever a função mdc usando um comando de repetição, como a seguir:

    int mdc (int a, int b) { int t; if (a<b) return mdc(b,a); while (b>0) { t = a; a = b; b = t % b; }; return a; }
  6. Defina uma função calc que receba como argumentos um caractere, que indica uma operação aritmética (’+’, ’-’, ’*’ e ’/’), e dois valores de ponto flutuante, e retorne o resultado da aplicação da operação sobre os dois argumentos. Por exemplo: calc     ('+', 1.0, 1.0) deve retornar 2.0 e calc     ('*',2.0,3.0) deve retornar 6.0.

    Solução: Vamos ilustrar aqui o uso do comando switch, existente em C, que permite escolher um dentre vários comandos para ser executado. O comando switch tem a forma:

    switch (e) { case e1: c1; case e2: c2; … case en: cn; }

    O comando switch pode ser visto como uma seqüência de comandos de seleção (if), cada um realizando um teste correspondente a um caso do comando switch.

    O significado de um comando switch é o seguinte: a expressão e é avaliada e é executado o primeiro comando ci, na ordem c1, …, cn, para o qual o valor fornecido por e é igual ao valor de ei (caso tal comando exista), sendo também executados todos os comandos seguintes (ci+1, …, cn), se existirem, nessa ordem.

    A execução de qualquer desses comandos pode ser finalizada, e geralmente deve ser, por meio de um comando break. No entanto, na solução desse exercício o uso de um comando break não é necessário, pois toda alternativa contém um comando return, que termima a execução da função.

    Supõe-se também, na função op definida abaixo, que o caractere passado como argumento para op é sempre igual a ’+’, ’*’, ’-’ ou ’/’.

    double op (char c, double a, double b) { switch (c) { case '+' : { return a + b; break; } case '*' : { return a * b; break; } case '-' : { return a - b; break; } case '/' : { return a / b; } } }

    Se o valor de e não for igual a nenhum dos valores ei, para in, um caso default pode ser usado. O caso default pode ser usado no lugar de case ei, para qualquer ei, para algum i=1,…,n, mas em geral deve ser usado como último caso. Se default não for especificado (é comum dizer “se não existir nenhum caso default”), o comando switch pode terminar sem que nenhum dos comandos ci, para i=1,…,n, seja executado (isso ocorre se o resultado da avaliação de e não for igual ao valor de nenhuma das expressões ei, para i=1,…,n).

    A necessidade do uso de um comando break, sempre que se deseja que seja executado apenas um dos comandos do comando switch, correspondente a um determinado caso (expressão), é em geral reconhecida como um ponto fraco do projeto da linguagem C.

    A expressão e, no comando switch, deve ter tipo int, short, byte ou char, devendo o seu tipo ser compatível com o tipo das expressões e1, …, en. As expressões e1, …, en têm que ser valores constantes (e distintos).

    Um comando switch pode ser também precedido de um rótulo — um nome seguido do caractere “:”. Nesse caso, um comando break pode ser seguido desse nome: isso indica que, ao ser executado, o comando break causa a terminação da execução do comando switch precedido pelo rótulo especificado.

  7. Escreva uma definição para a função raizq, que calcula a raiz quadrada de um dado valor x, com erro de aproximação menor que 0,0001 (0.0001 em C).

    A raiz quadrada de x é um número y tal que y2 = x. Para certos números, como, por exemplo, 2, a sua raiz quadrada é um número que teria que ser representado com infinitos algarismos na sua parte decimal, não sendo possível obter essa representação em um tempo finito. Desse modo, o cálculo desses valores é feito a seguir, com um erro de aproximação inferior a um valor preestabelecido — nesse caso, 0,0001. Vamos chamar de e o valor 0,0001. Denotando por | x | o valor absoluto de x, o valor y a ser retornado por raizq(x) deve ser tal que:

    y ≥ 0  e  | y2 − x | < e 

    Para definir a função raizq, use o método de aproximações sucessivas de Newton. Para a raiz quadrada, o método de Newton especifica que, se yi é uma aproximação para √x, então uma aproximação melhor é dada por:

    yi+1 = (yi + x/yi) / 2 

    Por exemplo, sejam x = 2 e y0 = 2. Então:

           y1 = (2 + 2/2) / 2= 1.5
           y2 = (1.5 + 2/1.5) / 2= 1.4167
           y3 = (1.4167 + 2/1.4167) / 2= 1.4142157 … 
     

    Repetindo esse processo, podemos obter aproximações para a raiz quadrada com qualquer precisão desejada (sujeitas, é claro, às limitações da representação de números do computador).

    Solução: A implementação da função raizq pode ser feita usando um comando de repetição, como mostrado abaixo.

    const double e = 0.0001; int fim (double y, double x) { return math.abs(y * y - x) < e; } double melhore (double y}, double x) { return (y + x/y) / 2; } double raizq (double x) { double y = x; while (! fim(y, x)) y = melhore (y, x); return y; }
  8. Em 1883, o matemático francês Édouard Lucas inventou a seguinte pequena estória:

    Um templo, na cidade de Hanói, contém três torres de diamante, em uma das quais Deus colocou, quando criou o mundo, 64 discos de ouro, empilhados uns sobre os outros, de maneira que os discos diminuem de tamanho da base para o topo, como mostrado a seguir. Os monges do templo trabalham sem cessar para transferir, um a um, os 64 discos da torre em que foram inicialmente colocados para uma das duas outras torres, mas de forma que um disco nunca pode ser colocado em cima de outro menor. Quando os monges terminarem de transferir todos os discos para uma outra torre, tudo virará pó, e o mundo acabará.

    A questão que se coloca é: supondo que os monges trabalhem tão eficientemente quanto possível, e consigam transferir 1 disco de uma torre para outra em 1 segundo, quanto tempo decorreria (em segundos) desde a criação até o fim do mundo?

    Solução: Uma solução recursiva para o problema é baseada na idéia ilustrada abaixo. Nessa solução, supõe-se, como hipótese indutiva, que se sabe como transferir n−1 discos de uma torre para outra (sem colocar um disco em cima de outro menor); o caso base consiste em transferir um disco de uma torre para outra vazia. O número total de movimentações de n discos é definido indutivamente por:

          f(1)= 1
          f(n)= 2 × f(n−1) + 1
     
    move n−1 discos da torre A para C, usando B
    move 1 disco de A para B
    move n−1 discos da torre C para B, usando A

    /home/camarao/ipcc/fig/hanoi-sol.eps

    Cada valor da seqüência de valores definida por f(n) representa, de fato, o menor número de movimentações requeridas para mover n discos de uma torre para outra, satisfazendo o requerimento de que um disco nunca pode ser colocado sobre outro de diâmetro menor. Para perceber isso, basta notar que, para mover um único disco d, digamos, da torre A para a torre B, é preciso antes mover todos os discos menores que d para a torre C. Portanto, a resposta à questão proposta anteriormente é dada por f(64). Um resultado aproximado é 1,844674 × 1019 segundos, aproximadamente 584,5 bilhões de anos.

    A definição de f estabelece uma relação de recorrência para uma seqüência de valores f(i), para i=1,…, n (uma relação de recorrência para uma seqüência é tal que cada termo é definido, por essa relação, em termos de seus predecessores). A primeira equação na definição de f é chamada condição inicial da relação de recorrência.

    Podemos procurar obter uma fórmula que define diretamente, de forma não-recursiva, o valor de f(n), buscando estabelecer um padrão que ocorre no cálculo de f(n), para cada n:

            f(1) = 1
            f(2) = 2 × f(1) + 1 = 2 × 1 + 1 = 2 + 1
            f(3) = 2 × f(2) + 1 = 2 (2 + 1) + 1  =  22 + 2 + 1
            f(4) = 2 × f(3) + 1 = 2 (22 + 2 + 1) + 1 = 23 + 22 + 2 + 1
            …
            f(n) = 2 × f(n−1) + 1 = 2n−1 + 2n−2 + … + 2 + 1

    Podemos observar que f(n) = 2n − 1, pois:

    n−1
    i=0
     2i = 2n − 1 

    O leitor interessado pode provar (usando indução sobre n) que, para todo n, a definição recursiva de f de fato satisfaz a equação f(n) = 2n − 1.

  9. O problema descrito a seguir foi introduzido em 1202 por Fibonacci — também conhecido como Leonardo de Pisa, e considerado como o maior matemático europeu da Idade Média:

    Supondo que um par de coelhos — um macho e uma fêmea — tenha nascido no início de um determinado ano, que coelhos não se reproduzam no primeiro mês de vida, que depois do primeiro mês um par de coelhos dê origem, a cada mês, a um novo par — macho e fêmea — e que nenhuma morte ocorra durante um ano, quantos coelhos vão existir no final do ano?

    Escreva um programa para solucionar esse problema, imprimindo o número de coelhos existentes no final do ano.

    Solução: Note que: 1) o número de (pares de) coelhos vivos no final de um mês k é igual ao número de (pares de) coelhos vivos no final do mês k−1 mais o número de (pares de) coelhos que nasceram no mês k; e 2) o número de (pares de) coelhos que nasceram no mês k é igual ao número de (pares de) coelhos vivos no mês k−2 (pois esse é exatamente o número de coelhos que geraram filhotes no mês k).

    Portanto, a quantidade de coelhos vivos ao fim de cada mês n é dada pelo número fib(n) da seqüência de números definida a seguir, conhecida como seqüência de Fibonacci:

              fib(0)= 0 
              fib(1)= 1 
              fib(n)fib(n−1) + fib(n−2)se  n > 1
     

    O problema pode então ser resolvido pelo programa a seguir:

    int fib (int n) { if (n==0) return 0; else if (n==1) return 1; else return fib(n-1) + fib(n-2); } int main() { printf("%d", fib(12)); }

    O número de pares de coelhos no final do décimo segundo mês, fib(12), é igual a 144 (ou seja, o número de coelhos é igual a 288).

    A função fib definido no programa acima é muito ineficiente, pois repete muitos cálculos, devido às chamadas recursivas a fib(n-1) e fib(n-2) (cada chamada de fib(n) envolve calcular fib(n-1) e fib(n-2), e a chamada a fib(n-2) envolve chamar fib(n-1) outra vez). A definição a seguir é muito mais eficiente:

    int fib1 (int n, int r1, int r) { return (n==0 ? r : fib1(n-1, r, r1+r); } int fib (int n) { return (n == 0 ? 0 : fib1(n,0,1)); } }

    As chamadas a fib1 usam r1 e r como acumuladores, evitando chamadas repetidas desnecessárias. A execução dessa definição recursiva é equivalente à obtida por meio do comando de repetição a seguir:

    int fib (int n) { int r1 = 0, r = 1, t, i; for (i=2; i<=n; i++) { t = r1; r1 = r; r = r + t; } return r; }
  10. Os comandos break e continue podem ser usados no corpo de um comando de repetição. A execução de um comando break causa o término da repetição e a execução de um comando continue causa o início imediato de uma próxima iteração. O comando continue provê uma forma de saltar determinados casos em um comando de repetição, fazendo com que o controle passe para a iteração seguinte.

    O exemplo a seguir imprime a representação unária (usando o símbolo “|”) de todos os números inteiros de 1 até n que não são múltiplos de um determinado valor inteiro m>1. O resultado da execução de Exemplo_continue 20 5 é mostrado abaixo.

    Os comandos break e continue podem especificar um rótulo, de mesmo nome do rótulo colocado em um comando de repetição. No caso de um comando break, o rótulo pode ser colocado também em um bloco ou em um comando switch (veja o Exercício Resolvido 6).

    Como ilustra esse exemplo, um comando continue sem um rótulo indica o início da execução da próxima iteração do comando de repetição mais interno em relação a esse comando continue. Analogamente, um comando break sem um rótulo indica o término da execução do comando de repetição ou comando switch mais interno em relação a esse comando break.


    int main() { int n, m, col = 1, i; printf("Impressao de valores de 1 a n em notacao unaria, sem multiplos de m\n"); printf("Digite n e m: "); scanf("%d%d", &n, &m); for (i=1; i<=n; i++) { printf("\n"); if (i % m == 0) continue; for (col=1; col<=i; col++) printf("|"); } return EXIT_SUCCESS; }
    Figura 4.2: Exemplo de uso do comando continue



    Impressao de valores de 1 a n em notacao unaria, sem multiplos de m
    Digite n e m: 20 5
    
    |
    ||
    |||
    ||||
    
    ||||||
    |||||||
    ||||||||
    |||||||||
    
    |||||||||||
    ||||||||||||
    |||||||||||||
    ||||||||||||||
    
    ||||||||||||||||
    |||||||||||||||||
    ||||||||||||||||||
    ||||||||||||||||||
    
    Figura 4.3: Resultado do programa de exemplo de uso do comando continue

    O comando break provê uma forma de sair de um comando de repetição, que pode ser, em algumas situações, mais fácil (ou mais conveniente do que outras alternativas). Em alguns casos, pode ser mais fácil testar uma condição internamente ao comando de repetição e usar o comando break, em vez de incluir essa condição no teste de terminação do comando de repetição.

    O uso do comando break é ilustrado pelo exemplo a seguir. Considere o problema de ler, repetidamente, um valor inteiro positivo e imprimir, para cada inteiro lido, seu fatorial. Se for lido um valor negativo, a execução do programa deve terminar.

    int fat (int x) { int fatx = 1, i; for (i=1; i<=x; i++) fatx\/} *= i; return fatx; } int main() { int v; while (1) { printf("Digite um valor inteiro: "); scanf("%d", &v); if (v < 0) break; printf("Fatorial de %d = %d, v, fat(v)); } return EXIT_SUCCESS; }
  11. Esse exercício ilustra o uso de entrada de dados até que uma condição ocorra ou até que se chegue ao final dos dados de entrada. O caractere Control-d é usado para especificar fim dos dados de entrada, em uma entrada de dados interativa no sistema operacional Linux e, no sistema operacional Windows, Control-z no início da linha seguido da tecla Enter (ou Return).

    Uma chamada à função scanf retorna o número de variáveis lidas, e pode ser usado para detectar fim dos dados de entrada a serem lidos (isto é, se não existe mais nenhum valor na entrada de dados a ser lido). O exemplo a seguir lê vários inteiros do dispositivo de entrada padrão e imprima a soma de todos os inteiros lidos. O programa termina quando não há mais valores a serem lidos.

    #include <stdio.h> #include <stdlib.h> int main() { int n, soma = 0, testeFim; while (1) { testeFim = scanf("%d", &n); if (testeFim != 1) break; soma = soma + n; } printf("Soma = %d\n", soma); return EXIT_SUCCESS; }

    O programa a seguir funciona de modo semelhante, mas lê vários inteiros positivos do dispositivo de entrada padrão e termina a execução quando quando não há mais valores a serem lidos ou quando um valor negativo ou zero for lido.

    #include <stdio.h> #include <stdlib.h> int main() { int n, soma = 0, testeFim; while (1)) { testeFim = scanf("%d", &n); if (testeFim != 1 || n<=0) break; else soma = soma + n; } printf("Soma = %d\n", soma); return EXIT_SUCCESS; }
  12. Escreva um programa que leia, do dispositivo de entrada padrão, vários valores inteiros, positivos ou não, e imprima, no dispositivo de saída padrão, os dois maiores valores lidos.

    A entrada termina com indicação de fim dos dados de entrada (em entrada interativa, Control-z seguido de Enter no Windows, ou Control-d no Linux).

    Solução: São usadas duas variáveis inteiras max1 e max2 para armazenar os dois maiores valores lidos, e elas são atualizadas adequadamente, se necessário, após a leitura de cada inteiro. O valor inicial atribuído a essas variáveis é o valor INT_MIN, menor inteiro armazenável em uma variável de tipo int. Qualquer valor inteiro digitado será maior ou igual a esse valor e será, caso for maior, atribuído à variável. INT_MIN é definido em limits.h.

    O valor retornado por scanf é usado para verificar fim dos dados de entrada: scanf retorna -1 como indicação de fim dos dados de entrada.

    O programa é mostrado a seguir.

    #include <stdio.h> #include <limits.h> int main() { int max1 = INT_MIN, max2 = INT_MIN, valor, fim; while (1) { fim = scanf("%d", &valor); if (fim == -1) break; if (valor > max1) { max2 = max1; max1 = valor; } else if (valor > max2) max2 = valor; } printf("Dois maiores = %d, %d\n", max1, max2); }
  13. Escreva um programa que leia, do dispositivo de entrada padrão, um texto qualquer, caractere a caractere e imprima, no dispositivo de saída padrão, i) o número de caracteres, ii) o número de palavras, e iii) o número de linhas do texto.

    Considere que uma palavra é uma sequência de um ou mais caracteres que começa com qualquer caractere que não é um delimitador de palavras. Um delimitador de palavras é um caractere espaço (branco, i.e. ’ ’), fim-de-linha ('\n') ou tab (caractere de tabulação, i.e. '\t').

    A entrada termina com indicação de fim dos dados de entrada (em entrada interativa, Control-z seguido de Enter no Windows, ou Control-d no Linux).

    Solução: A solução mostrada a seguir usa scanf com formato %c para ler um caractere, e o valor retornado por scanf para verificar fim dos dados de entrada (scanf retorna -1 para indicar fim dos dados).

    A função isletter definida retorna verdadeiro (em C, valor inteiro diferente de zero) se e somente se o caractere passado como argumento é uma letra. Para isso, é testado se tal caractere está entre ’a’ e ’z’ ou entre ’A’ e ’Z’.

    Para contar palavras, é usado uma variável (fora) que indica se o caractere corrente, que está sendo lido, está fora ou dentro de uma palavra. O número de palavras (armazanado na variável palavras) é incrementado quando se está fora de uma palavra e um caractere não delimitador de palavras é lido (como especificado no enunciado, um delimitador de palavras é considerado como sendo um dos caracteres espaço, fim-de-linha ou tab).

    O programa é mostrado a seguir.

    #include <stdio.h> int delim (char c) { return (c == '\ ') || (c == '\t') || (c == '\n'); } int main() { char c; int caracs = 0, palavras = 0, linhas = 0, isDelim, fim, fora = 1; while (1) { fim = scanf("%c", &c); if (fim == -1) break; caracs++; isDelim = delim(c); if (isDelim) { if (c=='\n') linhas++; fora=1; } else if (fora) { palavras++; fora = 0; } } printf("Numero de caracteres, palavras, linhas = %d, %d, %d\n", caracs, palavras, linhas); }
  14. Esse é um exercício baseado no problema PAR (Par ou ímpar) , obtido de:
    http://br.spoj.pl/problems/PAR
    Há uma modificação motivada pelo fato de que não vamos usar ainda leitura de cadeias de caracteres, e por isso os nomes dos jogadores correspondentes a escolha "par" e "ímpar" são substituídos respectivamente por "Par" e "Impar". O problema é descrito a seguir.

    O problema consiste em determinar, para cada jogada de partidas do jogo Par ou Ímpar, o vencedor da jogada.

    A entrada representa uma sequência de dados referentes a partidas de Par ou Ímpar. A primeira linha de cada partida contém um inteiro n, que indica o número de jogadas da partida. As n linhas seguintes contêm cada uma dois inteiros a e b que representam o número escolhido por cada jogador (0 ≤ a ≤ 5 e 0 ≤ B ≤ 5). O final da entrada é indicado por n = 0.

    A saída deve conter para cada partida, uma linha no formato Partida i, onde i é o número da partida: partidas são numeradas sequencialmente a partir de 1. A saída deve conter também uma linha para cada jogada de cada partida, contendo Par ou Impar conforme o vencedor da partida seja o jogador que escolheu par ou ímpar, respectivamente. Deve ser impressa uma linha em branco entre uma partida e outra.

    Por exemplo, para a entrada:

    3
    2 4
    3 5
    1 0
    2
    1 5
    2 3
    0

    a saída deve ser:

    Partida 1
    Par
    Par
    Impar
    Partida 2
    Par
    Impar

    Solução: O programa abaixo define e usa a função processaPartida para separar o processamento (cálculo e impressão) de valores de cada partida, e a função par, que determina se um dado valor é par ou não. O número de cada partida é armazenado em uma variável, que tem valor inicial igual a 1 e é incrementada após o processamento de cada partida.

    O programa usa também o recurso de definir a assinatura (ou interface, ou cabeçalho) de cada função definida, para usar (chamar) a função antes de defini-la. Na definição da interface o nome dos parâmetros é opcional, e é omitido no programa abaixo.

    #include <stdio.h> #include <stdlib.h> void processaPartida(int,int); int par(int); int main() { int numPartida = 1, numJogadas; while (1) { scanf("%d", &numJogadas); if (numJogadas == 0) break; processaPartida (numPartida, numJogadas); numPartida++; } } void processaPartida (int numPartida, int numJogadas) { int mao1, mao2; printf("Partida %d\n", numPartida); for ( ; numJogadas>0; numJogadas--) { scanf("%d%d", &mao1, &mao2); printf("%s\n\", par(mao1+mao2 ? "Par" : "Impar"); } printf("\n"); } int par (int valor) { return (valor % 2 == 0); }
  15. Nesse exercício vamos apresentar uma solução para o problema RUMO9S (Rumo aos 9s) , obtido de
    http://br.spoj.pl/problems/RUMO9s/ 
    A solução apresentada não usa arranjo nem cadeia de caracteres (abordados no próximo capítulo), para ler, armazenar e imprimir os valores de entrada. Em vez disso, caracteres são lidos um a um (números não podem ser lidos e armazenados como valores inteiros porque podem ter até 1000 dígitos decimais, e portanto não poder ser armazenadas como valores de tipo int ou long int). O problema é descrito a seguir.

    Um número inteiro é múltiplo de nove se e somente se a soma dos seus dígitos é múltiplo de 9. Chama-se grau-9 de um número inteiro n≥ 0 o valor igual a 1 se n=9, 0 se n<9, e 1 mais o grau-9 da soma de seus dígitos, se n>9.

    Escreva um programa que, dado uma sequência de inteiros positivos, imprime se cada um deles é múltiplo de nove e, em caso afirmativo, seu grau-9. A entrada, no dispositivo de entrada padrão, contém uma sequência de inteiros positivos, um em cada linha, e termina com o valor 0. Exemplo:

    Entrada:

    999
    27
    9
    998
    0

    Saída:

    999 e’ multiplo de 9 e seu grau-9 e’ 3.
    27 e’ multiplo de 9 e seu grau-9 e’ 2.
    9 e’ multiplo de 9 e seu grau-9 e’ 1.
    998 nao e’ multiplo de 9.

    Solução: A solução usa a função isdigit para testar se um dado caractere é um dígito (i.e. em C, um inteiro sem sinal, entre ’0’ e ’9’).

    A função somaEImprimeCaracs lê e imprime os dígitos contidos em uma linha da entrada padrão, e retorna o inteiro correspondente. Para isso, ela subtrai converte cada caractere lido no inteiro correspondente, subtraindo o caractere ’0’ (uma vez que os caracteres são ordenados, a partir de ’0’, no código ASCII, usado em C para representação de caracteres, como valores inteiros).

    #include <stdio.h> #include <stdlib.h> int somaEImprimeCaracs() { char c; scanf("%c", &c); if (c=='0') return 0; int soma=0; while (isdigit(c)) { soma += c - '0'; printf("%c",c); scanf("%c", &c); } return soma; } int somaDigs (int n) { return (n<10 ? n : (n%10 + somaDigs(n/10)); } int grau9 (int n) { if (n<10) return (n==9 ? 1 : 0); else { int grau = grau9 (somaDigs(n)); return (grau == 0? 0 : 1 + grau); } } int main() { int v, grau9v; while (1) { int n = somaEImprimeCaracs(); if (n==0) break; int grau9n = grau9(n); if (grau9n==0) printf(" is not a multiple of 9.\n", n); else printf(" is a multiple of 9 and has 9-degree %d.\n", grau9n); } return 0; }

4.5  Exercícios

  1. Escreva três definições de função, chamadas somaIter, somaRec e soma, tais que, dados dois números inteiros positivos a e b, retorne o valor a + b. As duas primeiras definições devem usar apenas as operações mais simples de incrementar 1 e decrementar 1 (devem supor que as operações de adicionar e de subtrair mais de uma unidade não são disponíveis). A primeira definição deve usar um comando de repetição, e a segunda definição deve ser recursiva. A terceira definição deve usar o operador + de adição.

    Inclua essas três definições em um programa de teste, e defina um programa de teste que leia vários valores a a b do dispositivo de entrada padrão, e não imprima nada se e somente se, para cada par de valores a e b lidos, os resultados retornados pelas execuções das três definições (somaIter, somaRec e soma) forem iguais. Se existir um par de valores a e b lido para o qual o resultado retornado pela execução das três definições não é igual, o programa deve terminar e uma mensagem deve ser impressa, informando o par de valores a, b para o qual o resultado da execução foi diferente, assim como o resultado retornado por cada definição, junto com o nome da função que retornou cada resultado.

    A leitura deve terminar quando não houver mais valores a serem lidos (em entrada interativa, quando o caractere que indica fim dos dados for digitado).

  2. Escreva um programa que leia pares de números inteiros positivos a,b e imprima, para cada par lido, o número de vezes que uma divisão inteira pode ser realizada, começando com o primeiro número como dividendo e usando sempre o segundo como divisor, e substituindo-se o dividendo pelo quociente na vez seguinte. A leitura deve terminar quando um dos valores lidos for menor ou igual a zero.

    O programa deve definir e usar uma função numdiv que recebe dois números inteiros positivos e retorna o número de vezes que uma divisão exata pode ser realizada, neste processo de substituir o dividendo pelo quociente. O processo de divisões sucessivas deve terminar quando uma divisão exata não existe.

    Exemplos: numdiv(8,2) deve retornar 3 e numdiv(9,2) deve retornar 0.

  3. O número de combinações de n objetos p a p — ou seja, o número de maneiras diferentes de escolher, de um conjunto com n elementos, um subconjunto com p elementos — comumente denotado por


    n
    p


    é dado pela fórmula:
    n(n−1)…(np+1)
    p!
     

    Por exemplo, o número de combinações de 4 objetos, 2 a 2, é igual a 4× 3/2! = 6 (se representamos os objetos por números, as combinações são {1,2}, {1,3}, {1,4}, {2,3}, {2,4} e {3,4}).

    Defina uma função que, dados n e p, calcule o número de combinações de n objetos p a p.

    Observação: A fórmula acima pode ser escrita também na forma:

    n!
    p! × (np)!
     

    No entanto, note que uma implementação baseada diretamente nessa última seria menos eficiente do que uma implementação baseada diretamente na primeira, uma vez que o número de operações de multiplicação necessárias para o cálculo seria maior nesse último caso.

    Escreva um programa que leia vários pares de números inteiros positivos n, p e imprima, para cada par lido, o número de combinações existentes de n objetos p a p. A leitura deve terminar quando um dos valores lidos for menor ou igual a zero.

  4. Escreva uma função para calcular qual seria o saldo de sua conta de poupança depois de 5 anos, se você depositou 1000 reais no início desse período e a taxa de juros é de 6% ao ano.
  5. Generalize a questão anterior, de maneira que se possa especificar quaisquer valores inteiros como capital inicial, taxa de juros e prazo desejados.

    Escreva um programa que leia, repetidamente, três valores inteiros positivos c, j, t que representam, respectivamente, o capital inicial, a taxa de juros anual e o número de anos de depósito, e imprima, para cada três valores lidos, o saldo final da conta, calculado usando a função acima. A leitura deve terminar quando um dos três valores lidos for menor ou igual a zero.

  6. Defina funções que, dado o número de termos n, calcule:
    1. i=1n i2
    2. 1/1 + 3/2 + 5/3 + 7/4 + …, usando n termos no somatório
    3. 1/1 − 2/4 + 3/9 − 4/16 + 5/25 − …, usando n termos no somatório
    4. i=0n (i/i! − i2/(i+1)!)
    5. π/4 = (2 × 4 × 4 × 6 × 6 × 8× …)/(3 × 3 × 5 × 5 × 7 × 7× …), usando n termos no produtório (considere termos 2/3, 4/3, 4/5, 6/5, 6/7 etc.)

    O resultado de cada item deve ser obtido usando i) um comando de repetição e ii) uma função recursiva.

    Escreva um programa que leia repetidamente pares de valores inteiros x, k e imprima, para cada par lido, o resultado de chamar a k-ésima função acima (k variando de 1 a 5) com o argumento x (que representa o número de termos). A leitura deve terminar quando x≤ 0 ou quando k não for um valor entre 1 e 5.

  7. Escreva funções para calcular um valor aproximado do seno e cosseno de um ângulo dado em radianos, usando as seguintes fórmulas:
           sen (x) = x − 
    x3
    3!
     + 
    x5
    5!
     − …
     
           cos (x) = 1 − 
    x2
    2!
     + 
    x4
    4!
     − …

    Cada função deve receber o valor de x em radianos e o número de parcelas a serem usadas no somatório.

    A definição não deve ser feita calculando o fatorial e a exponencial a cada parcela, mas sim de modo que o valor do numerador e do denominador de cada parcela sejam obtidos a partir dos valores respectivos da parcela anterior.

    Escreva um programa que leia, do dispositivo de entrada padrão, um número inteiro positivo n, em seguida vários números de ponto flutuante (um a um), que representam valores de ângulos em graus e, para cada valor, imprima o seno e o cosseno desse valor, usando as funções definidas acima com o número de parcelas igual a n. Note que você deve converter graus em radianos nas chamadas às funções para cálculo do seno e cosseno, e que essas funções devem ter um parâmetro a mais que indica o número de parcelas a ser usado.

    A entrada deve terminar quando um valor negativo ou nulo for lido.

  8. Escreva um programa que leia uma sequência de valores inteiros diferentes de zero, separados por espaços ou linhas, e imprima os valores pares dessa sequência. Um valor é par se o resto da divisão desse valor por 2 é igual a zero. A entrada termina quando um valor igual a zero for lido.

    Por exemplo, para a entrada:

    1 72 20 15 24 10 3 0 13 14

    a saída deve conter (os números pares da entrada antes do primeiro zero, ou seja):

    72 20 24 10
  9. Faça um programa para imprimir a seguinte tabela:
    12345678910
    2468101214161820
    102030405060708090100
  10. Escreva um programa que leia um número inteiro positivo n e imprima um triângulo como o mostrado abaixo, considerando que o número de linhas é igual a n.
    *
    ***
    *****
    *******
    *********
    ***********
    *************
  11. Escreva um programa que leia um número inteiro positivo n e imprima um losango como o mostrado abaixo, considerando que o número de linhas é igual a n (n=13 para o losango mostrado abaixo). Se n for par, as duas linhas no meio do losango devem ter o mesmo número de asteriscos.
    *
    ***
    *****
    *******
    *********
    ***********
    *************
    ***********
    *********
    *******
    *****
    ***
    *
  12. Defina uma função que converta o valor de uma temperatura dada em graus Fahrenheit para o valor correspondente em graus centígrados (ou Celsius). A conversão é dada pela fórmula:
    TC = 
    5 × (TF − 32)
    9
     

    Use a função definida acima como parte de um programa que receba como argumentos o valor de uma temperatura inicial, o valor de uma temperatura final e um passo p (valor de incremento), e imprima uma tabela de conversão de graus Fahrenheit em graus centígrados, desde a temperatura inicial até o maior valor que não ultrapasse a temperatura final, de p em p graus Fahrenheit.

  13. Os números mostrados na tabela a seguir formam a parte inicial do chamado triângulo de Pascal (nome dado em homenagem a Blaise Pascal (1623–1662), que escreveu um influente tratado sobre esses números). A tabela contém os valores das combinações de n elementos, p a p, para valores crescentes de n e p.
       0   1          
       1   11         
       2   121        
       3   1331       
       4   14641      
       5   15101051     
       6   1615201561    
       7   172135352171   
       8   18285670562881  
       9   193684126126843691 
       10   1104512021025221012045101

    As entradas em branco nessa tabela têm, de fato, valor igual a zero, tendo sido deixadas em branco para evidenciar o triângulo formado pelas demais entradas da tabela.

    Escreva um programa para imprimir o triângulo de Pascal, usando o fato de que



    n
    p+1




    n
    p


    × (np)
    p+1

    Observação: A fórmula acima pode ser deduzida facilmente de



    n
    p


    n!
    p ! × (np)!
  14. Escreva um programa que leia quatro valores inteiros positivos nA, nB, tA e tB — representando respectivamente as populações atuais de dois países A e B e as taxas de crescimento anual dessas populações — e determine o número de anos necessários para que a população do país A ultrapasse a de B, supondo que as taxas de crescimento dessas populações não variam e que nA < nB e tA > tB.
  15. Escreva um programa que leia, do dispositivo de entrada padrão, um texto qualquer, caractere a caractere e imprima, no dispositivo de saída padrão, i) o número de vogais, ii) o número de consoantes e iii) o número de outros caracteres diferentes de vogais e consoantes presentes em palavras: considere que estes são todos os demais caracteres que não sejam espaço, fim-de-linha ('\n') ou tab (caractere de tabulação, i.e. '\t').

    A entrada termina com indicação de fim dos dados de entrada (em entrada interativa, Control-z seguido de Enter no Windows, ou Control-d no Linux).

    Dicas: Use scanf com formato %c para ler um caractere, e use o valor retornado por scanf para verificar fim dos dados de entrada: scanf retorna -1 para indicar fim dos dados.

    Defina e use função que retorna verdadeiro se e somente se o caractere passado como argumento é uma letra; para defini-la, teste se tal caractere está entre ’a’ e ’z’ ou entre ’A’ e ’Z’.

  16. Modifique o programa da questão anterior de modo a eliminar a suposição de que nA < nB e calcular, nesse caso, se a menor população vai ou não ultrapassar a maior e, em caso afirmativo, o número de anos necessário para que isso ocorra (em caso negativo, o programa deve dar como resultado o valor 0).
  17. Resolva o problema BIT disponível em:
    http://br.spoj.pl/problems/BIT/ 
    O enunciado é apresentado, de forma resumida, a seguir.

    O problema consiste em escrever um programa para calcular e imprimir, para cada valor inteiro positivo lido, quantas notas de 50, 10, 5 e 1 reais são necessárias para totalizar esse valor, de modo a minimizar a quantidade de notas.

    A entrada é composta de vários conjuntos de teste. Cada conjunto de teste é composto por uma única linha, que contém um número inteiro positivo v, que indica o valor a ser considerado. O final da entrada é indicado por v = 0.

    Para cada conjunto de teste da entrada seu programa deve produzir três linhas na saída. A primeira linha deve conter um identificador do conjunto de teste, no formato "Teste n", onde n é o número do teste; os testes são numerados sequencialmente a partir de 1. Na segunda linha devem aparecer quatro inteiros, que representam o resultado encontrado pelo seu programa: o primeiro inteiro indica o número de notas de 50 reais, o segundo o número de notas de 10 reais, o terceiro o número de notas de 5 reais e o quarto o número de notas de 1 real. A terceira linha deve ser deixada em branco.

    Por exemplo, para a entrada:

    1
    72
    0

    a saída deve ser:

    Teste 1
    0 0 0 1
    Teste 2
    1 2 0 2
  18. Resolva o problema ALADES disponível em:
    http://br.spoj.pl/problems/ALADES/ 
    O enunciado é apresentado, de forma resumida, a seguir.

    O problema consiste em escrever um programa que, dados valores de hora e minutos corrente e hora e minutos de um alarme, determinar o número de minutos entre os dois valores.

    A entrada contém vários casos de teste. Cada caso de teste é descrito em uma linha, contendo quatro números inteiros h1, m1, h2 e m2, sendo que h1:m1 representa hora e minuto atuais, e h2:m2 representa hora e minuto para os quais um alarme foi programado (0≤ h1 < 24, 0≤ m1 < 60, 0≤ h2 < 24, 0 ≤ m2 ≤ 60). O final da entrada é indicado por uma linha que contém apenas quatro zeros, separados por espaços em branco. Os dados devem ser lidos da entrada padrão.

    Para cada caso de teste da entrada, deve ser impressa uma linha, no dispositivo de saída padrão, contendo um número inteiro que indica o número de minutos entre os dois horários.

    Por exemplo, para a entrada:

    1 5 3 5
    23 59 0 34
    21 33 21 10
    0 0 0 0

    a saída deve ser:

    120
    35
    1417
  19. Escreva um programa que leia um texto (sequência de caracteres) do dispositivo de entrada padrão e imprima, no dispositivo de saída padrão, qual é a vogal ou quais são as vogais mais frequentes no texto: o programa deve imprimir quais foram as vogais com maior frequência, se existir uma ou mais de uma com a mesma maior frequência.

    A ocorrência de uma vogal pode ser em letra minúscula ou maiúscula; em ambos os casos o número de ocorrências dessa vogal deve ser incrementado.

    A impressão das vogais com maior frequência pode usar a vogal minúscula ou maiúscula (escolhendo é claro um dos casos para todas as vogais com maior frequência).

    A leitura deve ser feita caractere a caractere, usando a função getChar. A leitura deve terminar quando getChar retornar EOF.

    getChar retorna um inteiro: EOF quando não há mais dados a serem lidos, caso contrário retorna o caractere lido (i.e. a representação do caractere no código ASCII).

    Use 5 variáveis para armazenar a frequência de cada vogal, e uma outra variável para indicar a maior frequência. Imprima as vogais com frequência igual a essa maior frequência.

  20. O mínimo múltiplo comum (mmc) entre dois ou mais números é (como o próprio nome diz) o menor inteiro que é múltiplo de todos eles. Por exemplo, mmc(4,6) é igual a 12. O máximo divisor comum (mdc) de dois ou mais números inteiros é (como o próprio nome diz) o maior divisor de todos eles (i.e. o maior valor que divide exatamente os números). Por exemplo, mdc(4,6) é igual a 2.

    Escreva um programa que leia uma sequência qualquer de números inteiros positivos e imprima o mínimo múltiplo comum entre eles.

    O programa deve ler os valores da entrada padrão e imprimir o resultado na saída padrão. Ele deve funcionar para entradas não interativas. Ou seja, a entrada pode estar em arquivo, especificado por redirecionamento da entrada padrão. O programa deve terminar com o fim dos dados de entrada (i.e. o término da entrada é indicado pelo valor retornado por scanf).

    O seu programa deve ser baseado nos fatos de que:

    1. mmc(a,b) = (a / mdc(a,b)) × b

      Ou seja, use o valor de mdc(a,b) para calcular mmc(a,b), dividindo a por mdc(a,b) e multiplicando por b.

    2. mmc de três ou mais números pode ser calculado usando o fato de que: mmc(a,b,c) = mmc(mmc(ab), c).

      Ou seja: para calcular mmc de três ou mais números, obtenha o mmc do resultado de calcular o mmc dos primeiros com o último.

    3. O cálculo do mdc de dois números a e b deve ser feito usando o algoritmo definido no Exercício Resolvido 5.

Previous Up Next