Escolar Documentos
Profissional Documentos
Cultura Documentos
163
164 | Capítulo 4 — Recursão e Retrocesso
R
ecursão é uma poderosa ferramenta em matemática e, especialmente, em programação. Quando
utilizada adequadamente, a recursão tem a capacidade de reduzir o tempo gasto na implementação
de algoritmos que, de outro modo, demandariam um tempo considerável. O uso de recursão pode
ainda tornar programas mais simples de entender. Por outro lado, recursão apresenta um inerente
efeito negativo em programação que é o fato de sempre demandar mais uso de recursos computacionais do que
soluções funcionalmente equivalentes que não usam recursão.
A técnica de retrocesso, por sua vez, tem como objetivo resolver uma certa categoria de problemas computacio-
nais utilizando o mínimo de recursos possível. Tal técnica é implementada por meio de recursão.
Este capítulo tem como enfoque essas duas importantes ferramentas de programação.
A maioria das linguagens de programação modernas permite a escrita de funções que chamam, direta ou in-
diretamente, a si mesmas. Tais funções são denominadas recursivas. Uma função recursiva deve conter pelo
menos duas partes (casos) a saber:
Caso base (não recursivo), que estabelece uma condição de parada (ou condição terminal) da re-
cursão e sem o qual a recursão será infinita. Esta parte da definição da função não deve fazer referência
à própria função.
Caso recursivo, no qual a função chama a si mesma. O programador deve garantir que uma das cha-
madas recursivas atinja certamente a condição de parada.
Uma função recursiva pode ter mais de um caso recursivo e mais de uma condição de parada.
A função SomaAteN() apresentada a seguir ilustra o processo de recursão em C:
int SomaAteN(int n)
{
if (n <= 1)
return n; /* Condição de parada */
else
return (n + SomaAteN(n - 1)); /* Caso recursivo */
}
A função SomaAteN() retorna o valor da soma dos inteiros compreendidos entre 1 e n, sendo n o parâmetro de
entrada da função. Por exemplo, a chamada SomaAteN(5) deve resultar em 15 (pois, 1 + 2 + 3 + 4 + 5 = 15).
Um diagrama de recursão é uma representação gráfica utilizada como artifício para facilitar o acompanhamento
de chamadas recursivas de uma função. O diagrama de recursão na Figura 4–1 (a) ilustra a sequência de cha-
madas recursivas que ocorre após a chamada inicial SomaAteN(5). Nesse diagrama, quando é feita a chamada
SomaAteN(1), a condição de parada é atingida e a função retorna 1. Com esse valor retornado, é possível voltar
sucessivamente ao passo anterior na representação esquemática acima até que a chamada original seja atingida.
Isto resulta no diagrama de recursão na Figura 4–1 (b).
Note que, a cada chamada recursiva da função SomaAteN(), o valor do parâmetro n é cada vez menor, de mo-
do que a condição de parada seja certamente atingida. Entretanto, a função SomaAteN() produz resultados
indesejáveis se o número introduzido for menor do que 1 (verifique isso). Uma forma de corrigir a função
SomaAteN() é modificando-a de modo que ela seja encerrada quando n < 1. Isto é feito na versão da função
SomaAteN() a seguir:
4.1 Funções Recursivas | 165
int SomaAteN2(int n)
{
if (n <= 0)
return 0; /* Erro de domínio */
if (n <= 1)
return n; /* Condição de parada */
return n + SomaAteN2(n - 1); /* Caso recursivo */
}
SomaAteN(5) SomaAteN(5)
5 + SomaAteN(4) 5 + SomaAteN(4) 15
4 + SomaAteN(3) 4 + SomaAteN(3) 10
3 + SomaAteN(2) 3 + SomaAteN(2) 6
2 + SomaAteN(1) 2 + SomaAteN(1) 3
1 1
(a) (b)
Figura 4–1: Acompanhamento de uma Função Recursiva
A função SomaAteN2() serve como exemplo introdutório do uso de recursão em C, mas essa evidentemente
não é a forma mais elegante de se resolver o problema da soma dos números inteiros compreendidos entre 1
e n. Isto é, esse problema é muito mais fácil de ser resolvido utilizando um laço iterativo ao invés de recursão.
Além de ser mais legível, uma versão iterativa da função SomaAteN2() irá provavelmente ser executada com
um melhor desempenho, pois a versão recursiva envolve o uso da pilha de execução para guardar parâmetros e
variáveis locais a cada chamada recursiva (v. Seção 4.3).
Numa chamada recursiva, um ou mais parâmetros devem ser alterados de modo a reduzir o tamanho do pro-
blema e fazer com que uma condição de parada seja atingida. Entretanto, é importante salientar que o fato de
se ter certeza que uma condição de parada seja teoricamente atingida não significa que, na prática, uma função
recursiva irá terminar. Quer dizer, pode ser que o número de chamadas recursivas seja tão grande que esgote a
capacidade da pilha de execução e o programa seja abortado.
Um erro comum na escrita de funções recursivas é o uso de laços de repetição, em vez de desvios condicionais.
Laços de repetição raramente aparecem numa função recursiva e esse tipo de erro é provavelmente decorrente
do fato de o programador inexperiente ser induzido a pensar que como funções recursivas envolvem repeti-
ção, essa repetição deve ser implementada por meio de laços de repetição, como ocorre com funções iterativas.
Os diagramas de recursão apresentados nas figuras desta seção são úteis para o acompanhamento de chamadas
recursivas, mas nem sempre esses diagramas são suficientes para um completo entendimento dessas chamadas.
Outros diagramas mais sofisticados e que têm a mesma finalidade são árvores de recursão (v. Seção 4.8.2) e
representações esquemáticas de pilhas de execução (v. Seção 4.3).
166 | Capítulo 4 — Recursão e Retrocesso
Uma função pode ser recursiva sem que chame a si mesma diretamente. Isto é, uma função pode ser conside-
rada recursiva se ela faz parte de uma cadeia recursiva de funções. Por exemplo, se uma função f() chama
uma outra função g() que, por sua vez, chama f(), ambas as funções f() e g() são consideradas recursivas e
formam uma cadeia recursiva.
O perigo de se ter recursão infinita é maior em cadeias recursivas do que com funções que são diretamente
recursivas. Também, em termos de estilo, cadeias recursivas não são fáceis de ser identificadas como tais, pois
examinando-se apenas uma das funções envolvidas não dá para perceber que a mesma chama indiretamente
a si mesma.
O espaço reservado para execução de um programa é dividido em partes que possuem finalidades próprias,
como mostra de modo simplificado a Figura 4–2.
Espaço de Execução Registro de Ativação
Strings constantes Parâmetros formais
Código do programa
Variáveis locais de duração automática
Variáveis de duração fixa
Valor de retorno (se houver)
Heap (alocação dinâmica)
Endereço de retorno
Espaço para expansão de heap/pilha
Registro de ativação N
Pilha de execução
Registro de ativação 2
Na Figura 4–2, há duas partes que aumentam ou diminuem de tamanho durante a execução do programa:
Heap[1]. Essa é a partição de memória é reservada para alocação dinâmica de memória (v. Capítulo
9). Essa porção de memória aumenta de tamanho à medida que espaço é alocado dinamicamente em
memória e diminui de tamanho quando esse espaço é liberado. O programador é diretamente respon-
sável pela variação de tamanho do heap. Essa partição será discutida em profundidade mais adiante
no Capítulo 9. A alocação de memória no heap é mais lenta do que na pilha porque envolve geren-
ciamento de memória mais complexo.
Pilha de execução. Essa partição no topo da figura é denominada pilha porque seu funcionamento
se assemelha ao de uma pilha de objetos. Isto é, os blocos armazenados nesse espaço são liberados na
ordem inversa de alocação (como ocorre, por exemplo, com uma pilha de pratos). Alocação na pilha
ocorre quando uma função é chamada e liberação ocorre quando uma função retorna. Ou seja, quando
uma função é chamada, nesse espaço são alocados os parâmetros da função, suas variáveis de duração
automática e o endereço da instrução que será executada quando a função retornar. Mais precisamente,
a pilha de execução de um programa é dividida em blocos contíguos em memória denominados regis-
tros de ativação. A cada chamada de função, é criado um registro de ativação para essa chamada con-
tendo: o endereço da instrução que fez a chamada, cópias dos parâmetros reais utilizados na chamada
e as variáveis locais de duração automática da função. Quando a função retorna, o espaço alocado em
memória para o registro de ativação da chamada é liberado. Em qualquer instante, a pilha de execu-
ção contém todos os registros de ativação associados a funções correntemente em execução (i.e., que
ainda não retornaram). Nessa porção de memória são armazenadas informações sobre cada chamada
de função efetuada no programa. A pilha de ativação é subdividida em partes denominadas registros
de ativação, sendo que cada um desses registros está associado a uma chamada de função. Em C, o
primeiro registro de ativação armazenado na pilha de execução está associado à função main() por-
que essa é a primeira função de um programa a ser chamada. Apenas o registro de ativação da função
main() permanece ativo durante toda a execução de um programa escrito em C. A razão pela qual essa
porção de memória é denominada pilha será discutida no Capítulo 9.
Sempre que uma função é chamada, um novo registro de ativação é criado e armazenado (i.e., empilhado) na
pilha de execução. Enquanto uma função não encerra sua execução (i.e., não retorna) seu registro de ativação
permanece armazenado na pilha. O conteúdo de um registro de ativação, ilustrado na porção direita da Figura
4–2, é o seguinte:
Parâmetros formais. Seção de um registro de ativação reservada para alocação dos parâmetros formais
da função (se houver algum).
Variáveis locais de duração automática. Se uma função possuir variáveis locais de duração automá-
tica, elas serão alocadas nessa seção do registro de ativação da função. Variáveis de duração fixa (locais
ou não) não são alocadas na pilha (v. lado esquerdo da Figura 4–2).
Valor de retorno. Se o tipo de retorno de uma função não for void, o registro de ativação da função
terá um espaço reservado para armazenamento do valor a ser retornado pela função.
Endereço de retorno. O endereço de retorno armazenado no registro de ativação de uma função é
o endereço da instrução para a qual o fluxo de execução do programa será desviado quando a função
retornar.
Durante a criação de um registro de ativação ocorre o seguinte:
1. Parâmetros formais são alocados e iniciados (i.e., recebem valores dos parâmetros reais)
[1] Existe uma estrutura de dados, que será estudada no Volume 2, cuja denominação também é heap. Essa estrutura de dados não possui
nenhuma relação com o conceito de heap discutido aqui.
168 | Capítulo 4 — Recursão e Retrocesso
No programa acima, utiliza-se uma versão da função SomaAteN2() apresentada na Seção 4.1. Essa nova versão
recebeu o acréscimo da variável s, que, conforme foi visto na Seção 4.1, é absolutamente desnecessária. Aqui,
o propósito dessa variável é meramente didático conforme será visto adiante. Aliás, a variável soma definida na
função main() também é desnecessária e serve o mesmo propósito didático.
A Figura 4–3 (a) mostra a pilha de execução do programa em questão antes da chamada da função SomaAteN3()
enquanto a Figura 4–3 (b) apresenta a mesma pilha logo após a execução dessa chamada. O endereço e1 que
aparece nessa última figura é o endereço de retorno da função SomaAteN3(); ou seja, e1 é o endereço da instrução:
soma = SomaAteN3(3); /* endereço e1 */
4.3 Pilha de Execução e Registros de Ativação | 169
n 3
s ? Registro de ativação
retorno ? de SomaAteN3()
endereço e1
Registro de ativação
soma ? soma ?
de main()
(a) (b)
Figura 4–3: Pilha de Execução e Registros de Ativação
A Figura 4–4 mostra a pilha de execução do programa em tela após as duas chamadas recursivas da função
SomaAteN3(). O endereço e2 que aparece nessa figura é o endereço de retorno de cada chamada recursiva;
quer dizer, e2 é o endereço da instrução:
s = n + SomaAteN3(n - 1); /* endereço e2 */
Quando uma chamada recursiva de função acrescenta um registro de ativação na pilha de execução, diz-se que
ela se encontra em sua fase de acréscimo. Por outro lado, quando a base da recursão de uma função recursiva é
atingida, e os registros de ativação passam a ser devidamente removidos da pilha de execução, diz-se que a fun-
ção está em fase de decréscimo. Uma função recursiva que apresenta apenas fase de acréscimo é uma função
com recursão infinita e o programa que a executa será encerrado por falta de espaço na pilha para alocação de
mais registros de ativação. Esse tipo de erro é conhecido como esgotamento de pilha (ou stack overflow, em
inglês). A Figura 4–4 mostra a função SomaAteN3() em sua fase de acréscimo enquanto a Figura 4–5 apre-
senta a fase de decréscimo dessa função.
n 1
s ?
retorno ?
endereço e3
n 2 n 2
s ? s ?
retorno ? retorno ?
endereço e2 endereço e2
n 3 n 3
s ? s ?
retorno ? retorno ?
endereço e1 endereço e1
soma ? soma ?
(a) (b)
Figura 4–4: Fase de Acréscimo de uma Função Recursiva
A discussão apresentada nesta seção leva à conclusão que manter informações sobre cada chamada de função
em registros de ativação pode consumir muito espaço em memória, especialmente quando se lida com progra-
mas contendo muitas chamadas recursivas. Além disso, empilhar e desempilhar registros de ativação na pilha
de execução são atividades que podem consumir tempo considerável em programas dessa natureza.
170 | Capítulo 4 — Recursão e Retrocesso
n 1
s 1
retorno 1
endereço e3
n 2 n 2
s ? s 3
retorno ? retorno 3
endereço e2 endereço e2
n 3 n 3 n 3
s ? s ? s 6
retorno ? retorno ? retorno 6
endereço e1 endereço e1 endereço e1
soma ? soma ? soma ? soma ?
(a) (b) (c) (d)
Figura 4–5: Fase de Decréscimo de uma Função Recursiva
Cada chamada de função, inclusive chamada de função recursiva, acarreta a criação de um registro de ativação.
Referências a parâmetros e variáveis locais de duração automática utilizam os valores nos respectivos registros
de ativação. Por exemplo, na chamada da função SomaAteN3():
SomaAteN3(5)
o valor armazenado no espaço reservado para o parâmetro n é 5. Então, a função SomaAteN3() é chamada
recursivamente como:
SomaAteN3(4)
Agora, o valor armazenado no espaço reservado para o parâmetro n é 4. Talvez, você esteja se perguntando: e
o que ocorre com o valor anterior de n que era 5? A resposta a esse aparente dilema é simples: os dois valores
coexistem. Lembre-se que o que caracteriza uma variável ou parâmetro são três atributos: (1) nome, (2) ende-
reço e (3) conteúdo (ou valor). Aqui, os dois valores do parâmetro n residem em registros de ativação diferentes
e, portanto, possuem endereços diferentes. Portanto não importa que eles apresentem o mesmo nome. Neste
contexto, pode-se pensar como se parâmetros e variáveis locais de duração automáticas em registros de ativação
diferentes tivessem escopos diferentes.
Para gerenciamento da pilha de execução, compilador age da seguinte maneira:
Para cada chamada de função, o compilador cria um prólogo, que é o código responsável pela criação
do registro de ativação (i.e., alocação de variáveis e parâmetros, casamento de parâmetros etc.)
Para cada retorno de função, o compilador cria um epílogo, que é o código responsável pela liberação
do espaço ocupado pelo registro de ativação e o devido retorno ao local no qual a função foi chamada
Conforme foi mostrado acima, recursão é uma técnica de programação que requer custos relativamente elevados
em termos de espaço em memória e tempo de processamento em relação a soluções iterativas equivalentes. Uma
situação na qual recursão pode ser facilmente transformada em iteração é quando a única chamada recursiva de
4.4 Recursão de Cauda | 171
uma função é a última instrução da função a ser executada, sem levar em consideração os casos não recursivos.
Esse tipo de recursão é denominado recursão é denominada recursão de cauda (ou tail recursion, em inglês).
Quando se diz no parágrafo acima que recursão de cauda ocorre quando a única chamada recursiva de uma
função é a última instrução executada da função, essa afirmação deve ser interpretada literalmente. Quer dizer,
quando uma chamada recursiva faz parte da última instrução de uma função, não se pode dizer que essa função
possui recursão de cauda. Por exemplo, a função Fatorial() a seguir não possui recursão de cauda:
int Fatorial(int n)
{
if (n < 0)
return -1; /* Erro de domínio */
if (!n)
return 1; /* Caso terminal */
return n*Fatorial(n - 1); /* Caso recursivo */
}
A função Fatorial() não possui recursão de cauda porque sua última instrução a ser executada é:
return n*Fatorial(n - 1);
que não é uma chamada recursiva, apesar de uma chamada recursiva fazer parte dessa instrução.
É importante salientar que recursão de cauda ocorre apenas quando a última instrução executada numa fun-
ção é uma chamada recursiva e essa chamada encerra a função. É importante notar ainda que a última instru-
ção a ser executada não precisa necessariamente aparecer na última linha de instrução. Por exemplo, a função
EmArrayRec() apresentada abaixo e discutida em detalhes na Seção 4.6 possui recursão de cauda:
int EmArrayRec(const int ar[], int inf, int sup, int num)
{
/* Verifica os casos base */
if (ar[inf] == num) /* Caso base 1 */
/* O valor procurado é o primeiro do array */
return inf; /* Encontrado um elemento igual a 'num' */
else if (inf >= sup) /* Caso base 2 */
/* Quando o índice inferior do array é maior */
/* do que ou igual ao seu índice superior, */
/* todo o array já foi examinado */
return -1; /* Elemento não foi encontrado */
/* Caso recursivo: procura no restante do array */
return EmArrayRec(ar, inf + 1, sup, num);
}
A função InverteArrayRec(), que aparece no programa apresentado a seguir, inverte a ordem dos elementos
e também apresenta recursão de cauda.
#include <stdio.h> /* printf() */
/****
* InverteArrayRec(): Inverte um array recursivamente
*
* Parâmetros:
* ar (entrada e saída) - array que será invertido
* inicio (entrada) - primeiro índice do array
* fim (entrada) - índice final do array
*
* Retorno: Nada
****/
172 | Capítulo 4 — Recursão e Retrocesso
A Figura 4–6 mostra a pilha de execução do programa acima com o registro de ativação da chamada da fun-
ção InverteArrayRec() logo após essa função ser chamada pela função main(). Por sua vez, a Figura 4–7
apresenta as pilhas de execução do programa em questão após a penúltima e a última chamadas recursivas da
função InverteArrayRec().
ar
inicio 0
Registro de ativação de
fim 4
InverteArrayRec()
aux ?
endereço e1
2 5 1 7 3
Registro de ativação
final 4
de main()
i ?
++inicio;
--fim;
}
}
ar
inicio 2
fim 2
aux ?
endereço e3
ar ar
inicio 1 inicio 1
fim 3 fim 3
aux ? aux 5
endereço e2 endereço e2
ar ar
inicio 0 inicio 0
fim 4 fim 4
aux ? aux 2
endereço e1 endereço e1
2 5 1 7 3 3 7 1 5 2
final 4 final 4
i ? i ?
(a) (b)
Figura 4–7: Registros de Ativação de uma Função com Recursão de Cauda 2
Para o programador, a importância de saber identificar recursão de cauda numa função recursiva é que ela fre-
quentemente pode ser transformada numa iteração mais eficiente. Isso acontece porque, quando uma chamada
recursiva é a última instrução a ser executada de uma função, sua execução não precisa mais dos dados armaze-
nados no registro de ativação criado na última chamada dessa função, de modo que ele pode ser desempilhado
da pilha de execução.
Transformar uma função que não apresenta recursão de cauda em função iterativa é mais complicado porque
requer o uso explícito de pilha (v. Capítulo 9). Além disso, a função iterativa resultante pode não ser fácil de
entender.
Bons compiladores são capazes de reconhecer recursão de cauda e otimizar o código gerado. Quer dizer, quando
identificam uma recursão de cauda esses compiladores não criam um novo registro de ativação associado a essa
chamada. Em vez disso, eles sobrescrevem o registro de ativação atual, uma vez que ele não terá mais utilidade.
Retrocesso (ou backtracking em inglês) é uma técnica de programação usualmente implementada por meio
de recursão e especialmente apropriada para uma categoria de problemas denominados problemas de satisfação
de restrições (v. Seção 4.5.2).
A técnica de retrocesso pode ser aplicada apenas para problemas que admitem candidatos a soluções parciais
e possuem testes para verificar se, num dado instante, os candidatos podem constituir uma solução completa.
Quer dizer, a técnica de retrocesso enumera um conjunto de candidatos parciais que, em princípio, podem ser
completados de várias maneiras para resultar em todas as possíveis soluções para um dado problema. Retrocesso
não serve, por exemplo, para problemas como busca e ordenação de dados.
174 | Capítulo 4 — Recursão e Retrocesso
Coluna 0
Coluna 1
Coluna 2
Coluna 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
(a) (b)
Linha 0 Linha 0
Linha 1 Linha 1
Linha 2 Linha 2
Linha 3 Linha 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
(c) (d)
Linha 0 Linha 0
Linha 1 Linha 1
Linha 2 Linha 2
Linha 3 Linha 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
(e) (f)
Linha 0 Linha 0
Linha 1 Linha 1
Linha 2 Linha 2
Linha 3 Linha 3
A Figura 4–11 (e) representa uma solução para o problema de posicionamento de quatro rainhas num (pseu-
do) tabuleiro 4 x 4. Mas essa não é a única solução para esse problema. De fato, existe outra solução que pode
ser obtida retrocedendo-se até o posicionamento da primeira rainha.
Coluna 0
Coluna 1
Coluna 2
Coluna 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
(a) (b)
Linha 0 Linha 0
Linha 1 Linha 1
Linha 2 Linha 2
Linha 3 Linha 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
(c) (d)
Linha 0 Linha 0
Linha 1 Linha 1
Linha 2 Linha 2
Linha 3 Linha 3
Coluna 0
Coluna 1
Coluna 2
Coluna 3
(e)
Linha 0
Linha 1
Linha 2
Linha 3
Quando aplicável, retrocesso é bem mais rápido do que qualquer técnica de força bruta na qual todas as pos-
síveis soluções são testadas.
Num problema de posicionamento de n rainhas num tabuleiro n x n, há n2 posições no tabuleiro passíveis
de ocupação, dentre as quais n devem ser escolhidas para acomodar as n rainhas. Portanto, quando n = 4, há
256 candidatos a solução e, conforme pode ser comprovado examinando-se as figuras acima, para encontrar
a primeira solução para o problema das quatro rainhas usando-se retrocesso, foram levados em consideração
apenas 27 movimentos.
Implementação da Solução
Aqui, se mostrará como o uso de recursão simplifica a codificação da técnica de retrocesso na resolução do pro-
blema das quatro rainhas. Em primeiro lugar, será necessário elaborar um meio para armazenar as diagonais
esquerdas e direitas do tabuleiro de modo que se possa indicar quais são as diagonais sob influência de uma dada
rainha. Diagonais esquerdas são aquelas que cruzam o tabuleiro da direita para a esquerda e diagonais direitas
são aquelas que cruzam o tabuleiro da esquerda para a direita, sendo que, nos dois casos, o cruzamento é efe-
tuado de cima para baixo. Examine a Figura 4–12 e note que as casas do tabuleiro cruzadas por cada diagonal
esquerda apresentam um valor constante para a soma da linha e da coluna que identificam a casa. Por exemplo,
a diagonal esquerda superior cruza apenas uma casa para a qual a linha e a coluna valem zero e a soma desses
valores também é zero. A próxima diagonal cruza as casas identificadas por linha e coluna como (0, 1) e (1, 0).
Nesses dois casos, a soma da linha com a coluna é 1. Seguindo esse raciocínio, as diagonais esquerdas podem
ser representadas por um array indexado de 0 a 6.
Coluna
Linha
Coluna
Coluna
Linha
Linha
Diagonal 0 Diagonal 3
(a) (b)
Figura 4–13: Diagonais Direitas no Problema das Quatro Rainhas
Os arrays discutidos acima são definidos na função main() do programa como:
tDisponibilidade colunas[N] = {DISPONIVEL},
diagEsq[2*N - 1] = {DISPONIVEL},
diagDir[2*N - 1] = {DISPONIVEL};
A constante simbólica N e o tipo tDisponibilidade usados nessa definição são definidos no início do pro-
grama como:
#define N 4 /* Número de linhas, colunas ou rainhas */
typedef enum {DISPONIVEL, INDISPONIVEL} tDisponibilidade;
As constantes do tipo enumeração tDisponibilidade definido acima indicam se uma posição num dos arrays
discutidos acima está disponível (i.e., pode acomodar uma rainha) ou não. Lembre-se que, como foi discutido
na Seção 1.13, a constante DISPONIVEL vale 0, ao passo que a constante INDISPONIVEL vale 1.
A função main(), que será apresentada a seguir, apenas inicia o número de soluções com zero e os arrays cujos
papéis desempenhados na resolução do problema foram discutidos acima. Quem de fato realiza a tarefa de re-
solver o problema é a função Posiciona(), que será abordada mais adiante.
int main(void)
{
tDisponibilidade colunas[N] = {DISPONIVEL},
diagEsq[2*N - 1] = {DISPONIVEL},
diagDir[2*N - 1] = {DISPONIVEL};
int pLinha[N], /* Posição de rainha numa linha */
ns = 0; /* Número de soluções encontradas */
/* Passa a bola para a função Posiciona() */
Posiciona( 0, colunas, diagEsq, diagDir, pLinha, &ns );
return 0;
}
O array pLinha[] armazena a posição da rainha em cada linha para cada solução do problema e não precisa
ser iniciado na função main() (v. adiante). Por sua vez, a variável ns representa o número de soluções encon-
tradas e é iniciada com 0, visto que inicialmente ainda não foi encontrada nenhuma solução. A função main()
chama a função Posiciona(), que é responsável por implementar a solução do problema discutido acima. Os
parâmetros dessa última função, que será apresentada adiante são:
linha (entrada) — linha na qual será efetuado o posicionamento
colunas[] (entrada e saída) — array que armazena a disponibilidade em cada coluna
180 | Capítulo 4 — Recursão e Retrocesso
diagonalE[] (entrada e saída) — array que armazena a disponibilidade em cada diagonal esquerda
diagonalD[] (entrada e saída) — array que armazena a disponibilidade em cada diagonal direita
pos[] (saída) — array que armazena a posição da rainha em cada linha
*solucoes (entrada e saída) — número de soluções encontradas
void Posiciona( int linha, tDisponibilidade colunas[], tDisponibilidade diagonalE[],
tDisponibilidade diagonalD[], int pos[], int *solucoes )
{
int coluna;
/* Procura uma coluna para a rainha na linha recebida como parâmetro */
for (coluna = 0; coluna < N; coluna++) {
/* Verifica se a coluna corrente está disponível */
if ( colunas[coluna] == DISPONIVEL &&
diagonalE [linha + coluna] == DISPONIVEL &&
diagonalD[linha - coluna + N - 1] == DISPONIVEL) {
/* Encontrada uma coluna disponível e */
/* a rainha é colocada nessa coluna */
pos[linha] = coluna;
/* Essa coluna passa a estar indisponível e o mesmo ocorre com */
/* as diagonais que cruzam a mesma coluna e a mesma linha */
colunas[coluna] = INDISPONIVEL;
diagonalE[linha + coluna] = INDISPONIVEL;
diagonalD[linha - coluna + N - 1] = INDISPONIVEL;
/* Se a linha corrente não for a última, */
/* posiciona uma rainha na próxima linha. Caso */
/* contrário, apresenta a solução corrente. */
if (linha < N - 1) {
/* Posiciona a rainha na próxima linha */
Posiciona( linha + 1, colunas, diagonalE, diagonalD, pos, solucoes );
} else { /* Essa era a última linha. Portanto */
/* foi encontrada mais uma solução. */
/* Incrementa o número de soluções encontradas até */
/* aqui e apresenta a solução corrente */
(*solucoes)++;
ApresentaSolucao(pos, *solucoes);
}
/* Torna a presente coluna e as diagonais que a cruzam */
/* disponíveis novamente para que ocorra retrocesso */
colunas[coluna] = DISPONIVEL;
diagonalE[linha + coluna] = DISPONIVEL;
diagonalD[linha - coluna + N - 1] = DISPONIVEL;
} /* if */
} /* for */
}
A função Posiciona() detém o papel principal na resolução do problema das quatro rainhas. Ela é crucial na
compreensão da técnica de retrocesso, mas, ao mesmo tempo, não é tão facilmente entendida devido ao fato
de alguns detalhes importantes serem ocultos por chamadas recursivas. Essa função será discutida mais adiante,
de modo que, por enquanto, observe outros detalhes menos importantes do programa.
A função ApresentaSolucao() complementa ao programa e é apenas uma função de apresentação na tela de
um tabuleiro que representa uma solução para o problema das N rainhas. O parâmetro posEmLinha[] dessa
4.5 Retrocesso (Backtracking) | 181
função é um array contendo a posição das rainhas em cada linha, enquanto nSolucao é o número da de or-
dem solução.
void ApresentaSolucao(const int posEmLinha[], int nSolucao)
{
int i, j;
/* Apresenta o número da solução */
printf("\nSolucao %d:\n\n", nSolucao);
/***********************/
/* Desenha o tabuleiro */
/***********************/
for(i = 0; i < N; ++i) { /* Desenha cada linha */
/* Apresenta cada coluna vazia antes da rainha */
for(j = 0; j < posEmLinha[i]; ++j)
printf("\t-");
printf("\tR"); /* Apresenta a rainha da linha */
/* Apresenta cada coluna vazia depois da rainha */
for(j = posEmLinha[i] + 1; j < N; ++j)
printf("\t-");
printf("\n"); /* Passa para a próxima linha */
}
}
Resultado de execução do programa:
Solucao 1:
- R - -
- - - R
R - - -
- - R -
Solucao 2:
- - R -
R - - -
- - - R
- R - -
A seguir, a função Posiciona() será escrutinada para que você possa, de fato, entender o seu funcionamento
bem como aquilo que diz respeito à técnica de retrocesso em si. Para completo entendimento do processo, é
interessante que se leve em consideração os diversos estados da pilha de execução do programa.
A Figura 4–14 (a) mostra o status da pilha de execução do programa no instante em que é efetuada a primeira
chamada da função Posiciona() no corpo da função main(). Note que, nessa figura e nas demais figuras que
serão apresentadas, os endereços de retorno das funções são omitidos porque eles não interessam à discussão
em questão. Observe ainda que o array pLinha[], que armazenará a solução do problema, não é iniciado na
função main() e essa é a razão pela qual aparecem pontos de interrogação como valores de seus elementos. De
fato, essa iniciação não tem importância, visto que esse array será utilizado pela função Posiciona() como
parâmetro de saída apenas. Por outro lado, todos os elementos dos arrays colunas[], diagEsq[] e diagDir[]
são iniciados com zero, que é o valor da constante de enumeração DISPONIVEL (v. acima).
182 | Capítulo 4 — Recursão e Retrocesso
coluna 0
linha 1
Posiciona()
(2ª chamada)
colunas
diagonalE
diagonalD
pos
solucoes
coluna ? coluna 0
linha 0 linha 0
Posiciona()
Posiciona()
(1ª chamada)
(1ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 0 0 0 0 colunas 1 0 0 0
diagEsq 0 0 0 0 0 0 0 diagEsq 1 0 0 0 0 0 0
main()
main()
diagDir 0 0 0 0 0 0 0 diagDir 0 0 0 1 0 0 0
pLinha ? ? ? ? pLinha 0 ? ? ?
ns 0 ns 0
(a) (b)
Figura 4–14: Problema das Quatro Rainhas: Pilha de Execução 1
Na Figura 4–14 (b), ocorre a primeira chamada recursiva da função Posiciona() (que, obviamente, é a se-
gunda chamada dessa função). Essa chamada acontece após o posicionamento da rainha na primeira coluna
da linha. Observe as alterações efetuadas nos arrays que se encontram armazenados no registro de ativação da
função main().
A Figura 4–15 (a) mostra a configuração da pilha de execução logo após a terceira chamada da função
Posiciona() (segunda chamada recursiva). Antes dessa chamada, a segunda rainha foi posicionada, conforme
se pode constatar examinado as alterações nos arrays no registro de ativação de main().
Na Figura 4–15 (b), após quatro tentativas malsucedidas de posicionamento da terceira rainha, a terceira cha-
mada da função Posiciona() é encerrada com o consequente desempilhamento de seu registro de ativação.
Em seguida, ocorre o primeiro retrocesso.
4.5 Retrocesso (Backtracking) | 183
Desempilhamento
coluna 0 coluna 4
linha 2 linha 2
Posiciona()
Posiciona()
(3ª chamada)
(3ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 2 coluna 2
linha 1 linha 1
Posiciona()
Posiciona()
(2ª chamada)
(2ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 0 coluna 0
linha 0 linha 0
Posiciona()
Posiciona()
(1ª chamada)
(1ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 1 0 1 0 colunas 1 0 1 0
diagEsq 1 0 0 1 0 0 0 diagEsq 1 0 0 1 0 0 0
main()
main()
diagDir 0 0 1 1 0 0 0 diagDir 0 0 1 1 0 0 0
pLinha 0 2 ? ? pLinha 0 2 ? ?
ns 0 ns 0
(a) (b)
Figura 4–15: Problema das Quatro Rainhas: Pilha de Execução 2
A Figura 4–16 (a) apresenta a execução do retrocesso promovido pela segunda chamada da função Posiciona().
O primeiro passo desse retrocesso consiste em desfazer as últimas alterações efetuadas nos arrays colunas[],
diagEsq[] e diagDir[]. Essas novas alterações fazem com que a rainha que se encontrava na coluna 2 da linha
1 seja removida dessa posição. As três últimas instruções da função são responsáveis pelo referido desfazimento.
Em seguida, essa rainha é reposicionada na coluna 3 dessa mesma linha. Novamente, os referidos arrays mais
o array pLinha[] são alterados para refletir esse novo posicionamento.
Na Figura 4–16 (b), a função Posiciona() é novamente chamada (quarta chamada). Essa chamada é respon-
sável pelo posicionamento da terceira rainha na coluna 1 da linha 2, como se pode constatar nas alterações dos
184 | Capítulo 4 — Recursão e Retrocesso
arrays colunas[], diagEsq[] e diagDir[] mostradas na Figura 4–17 (a). Essa última figura mostra ainda
que mais uma chamada de Posiciona() (quinta chamada) é levada a efeito. Na Figura 4–17 (b), ocorre o
retorno dessa última chamada com subsequente desempilhamento do seu registro de ativação. Esse retorno é
devido às tentativas frustradas de alocação da quarta rainha na linha 3.
coluna 0
linha 2
Posiciona()
(4ª chamada)
colunas
diagonalE
diagonalD
pos
solucoes
coluna 3 coluna 3
linha 1 linha 1
Posiciona()
Posiciona()
(2ª chamada)
(2ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 0 coluna 0
linha 0 linha 0
Posiciona()
Posiciona()
(1ª chamada)
(1ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 1 0 0 1 colunas 1 0 0 1
diagEsq 1 0 0 0 1 0 0 diagEsq 1 0 0 0 1 0 0
main()
main()
diagDir 0 1 0 1 0 0 0 diagDir 0 1 0 1 0 0 0
pLinha 0 3 ? ? pLinha 0 3 ? ?
ns 0 ns 0
(a) (b)
Figura 4–16: Problema das Quatro Rainhas: Pilha de Execução 3
4.5 Retrocesso (Backtracking) | 185
Desempilhamento
coluna 0 coluna 4
linha 3 linha 3
Posiciona()
Posiciona()
(5ª chamada)
(5ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 1 coluna 1
linha 2 linha 2
Posiciona()
Posiciona()
(4ª chamada)
(4ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 3 coluna 3
linha 1 linha 1
Posiciona()
Posiciona()
(2ª chamada)
(2ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 0 coluna 0
linha 0 linha 0
Posiciona()
Posiciona()
(1ª chamada)
(1ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 1 1 0 1 colunas 1 1 0 1
diagEsq 1 1 0 0 1 0 0 diagEsq 1 1 0 0 1 0 0
main()
main()
diagDir 0 1 0 1 1 0 0 diagDir 0 1 0 1 1 0 0
pLinha 0 3 1 ? pLinha 0 3 1 ?
ns 0 ns 0
(a) (b)
Figura 4–17: Problema das Quatro Rainhas: Pilha de Execução 4
186 | Capítulo 4 — Recursão e Retrocesso
Na Figura 4–18 (a), ocorre o segundo retrocesso levado a efeito pela quarta chamada de Posiciona(). Nesse
retrocesso, são desfeitas as alterações efetuadas por essa chamada nos arrays colunas[], diagEsq[] e diagDir[].
Ocorre, porém, que essa chamada não consegue realocar a terceira rainha na linha 2. Assim ocorre o retorno
dessa chamada com o respectivo desempilhamento do registro de ativação, como mostra a Figura 4–18 (a).
A Figura 4–18 (b) mostra o terceiro retrocesso, desta vez promovido pela segunda chamada de Posiciona().
Nesse retrocesso, essa chamada de função desfaz as alterações que ela havia efetuado nos arrays colunas[],
diagEsq[] e diagDir[]. Entretanto, esse retrocesso não é capaz de reposicionar a rainha que se encontra na
coluna 3 da linha 1. Desse modo, essa chamada retorna e ocorre o desempilhamento do seu registro de ativação.
Desempilhamento
coluna 4
linha 2
Posiciona()
(4ª chamada)
colunas
diagonalE
diagonalD
Desempilhamento
pos
solucoes
coluna 3 coluna 4
linha 1 linha 1
Posiciona()
Posiciona()
(2ª chamada)
(2ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 0 coluna 0
linha 0 linha 0
Posiciona()
(1ª chamada)
(1ª chamada)
Posiciona()
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 1 1 0 1 colunas 1 0 0 1
diagEsq 1 1 0 0 1 0 0 diagEsq 1 0 0 0 1 0 0
main()
main()
diagDir 0 1 0 1 1 0 0 diagDir 0 1 0 1 0 0 0
pLinha 0 3 1 ? pLinha 0 3 1 ?
ns 0 ns 0
(a) (b)
Figura 4–18: Problema das Quatro Rainhas: Pilha de Execução 5
4.5 Retrocesso (Backtracking) | 187
A Figura 4–19 (a) ilustra mais um retrocesso. Dessa vez, a primeira chamada de é responsável por sua execução.
Primeiro, as três últimas instruções dessa chamada de função desfazem o último posicionamento da primeira
rainha. Em seguida, essa chamada é bem-sucedida na tentativa de realocar essa rainha, de modo que ela passa a
ocupar a coluna 1 da linha 0. Finalmente, ocorre a sexta chamada da função Posiciona() [v. Figura 4–19 (b)].
coluna 0
linha 1
Posiciona()
(6ª chamada)
colunas
diagonalE
diagonalD
pos
solucoes
coluna 1 coluna 1
linha 0 linha 0
Posiciona()
Posiciona()
(1ª chamada)
(1ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 0 1 0 0 colunas 0 1 0 0
diagEsq 0 1 0 0 0 0 0 diagEsq 0 1 0 0 0 0 0
main()
main()
diagDir 0 0 1 0 0 0 0 diagDir 0 0 1 0 0 0 0
pLinha 1 3 1 ? pLinha 1 3 1 ?
ns 0 ns 0
(a) (b)
Figura 4–19: Problema das Quatro Rainhas: Pilha de Execução 6
A Figura 4–20 (a) mostra o instante em que a sexta chamada da função Posiciona() efetua a sétima chamada
dessa função, após ter posicionado a segunda rainha na coluna 3 da linha 1. Observe as alterações nos arrays
armazenados no registro de ativação da função main(). Na Figura 4–20 (b), após posicionar a terceira rainha
na coluna 0 da linha 2, a sétima chamada de Posiciona() realiza a oitava chamada dessa função.
A Figura 4–21 (a) mostra a situação logo após a oitava chamada de Posiciona() posicionar a quarta rainha
na coluna 2 da linha 3. Por outro lado, a Figura 4–21 (b) mostra essa chamada prestes a retornar. Observe
que, diferentemente do que ocorreu nas chamadas anteriores nas quais a parte if da segunda instrução if-else
foi executada, agora a parte else dessas instrução é executada, de modo que é incrementado o conteúdo apon-
tado pelo parâmetro solucoes, que corresponde ao conteúdo da variável ns definida na função main(). Esse
fato indica que foi encontrada a primeira solução para o problema, o que realmente aconteceu. Além disso, a
função ApresentaSolucao() é invocada para exibir essa solução na tela.
A Figura 4–21 (b) mostra ainda que após o retorno da oitava chamada de Posiciona(), a sétima e a sexta
chamadas dessa função também irão retornar (nessa ordem). Mas, antes que ocorra o retorno, ocorre retro-
cesso em cada uma delas. Cada função tenta reposicionar a rainha na linha sob responsabilidade da respectiva
função. É interessante lembrar ainda que, no retrocesso, cada função desfaz a alteração que efetuou nos arrays
colunas[], diagEsq[] e diagDir[].
188 | Capítulo 4 — Recursão e Retrocesso
Após os desempilhamentos mostrados na Figura 4–21 (b), a configuração passa a ser aquela ilustrada na Figura
4–22. A partir daí até que seja encontrada a segunda e última solução para o problema, a história apresentada
aqui se repetirá.
coluna 0
linha 3
Posiciona()
(8ª chamada)
colunas
diagonalE
diagonalD
pos
solucoes
coluna 0 coluna 0
linha 2 linha 2
Posiciona()
Posiciona()
(7ª chamada)
(7ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 3 coluna 3
linha 1 linha 1
Posiciona()
Posiciona()
(6ª chamada)
(6ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 1 coluna 1
linha 0 linha 0
Posiciona()
Posiciona()
(1ª chamada)
(1ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 0 1 0 1 colunas 1 1 0 1
diagEsq 0 1 0 0 1 0 0 diagEsq 0 1 0 0 1 0 0
main()
main()
diagDir 0 1 0 1 0 0 0 diagDir 0 1 0 1 0 0 0
pLinha 1 3 1 ? pLinha 1 3 0 ?
ns 0 ns 0
(a) (b)
Figura 4–20: Problema das Quatro Rainhas: Pilha de Execução 7
4.5 Retrocesso (Backtracking) | 189
Desempilhamento
coluna 2 coluna 2
linha 3 linha 3
Posiciona()
Posiciona()
(8ª chamada)
(8ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 0 coluna 0
Desempilhamento
linha 2 linha 2
Posiciona()
Posiciona()
(7ª chamada)
(7ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 3 coluna 3
Desempilhamento
linha 1 linha 1
Posiciona()
Posiciona()
(6ª chamada)
(6ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
coluna 1 coluna 1
linha 0 linha 0
Posiciona()
Posiciona()
(1ª chamada)
(1ª chamada)
colunas colunas
diagonalE diagonalE
diagonalD diagonalD
pos pos
solucoes solucoes
colunas 1 1 1 1 colunas 1 1 1 1
diagEsq 0 1 0 0 1 1 0 diagEsq 0 1 0 0 1 0 0
main()
main()
diagDir 0 1 0 1 1 0 0 diagDir 0 1 0 1 1 0 0
pLinha 1 3 0 2 pLinha 1 3 0 2
ns 1 ns 1
(a) (b)
Figura 4–21: Problema das Quatro Rainhas: Pilha de Execução 8
coluna 1
linha 0
Posiciona()
(1ª chamada)
colunas
diagonalE
diagonalD
pos
solucoes
colunas 0 0 0 0
diagEsq 0 0 0 0 0 0 0
main()
diagDir 0 0 0 0 0 0 0
pLinha 1 3 0 2
ns 1
Em cursos introdutórios de programação, você certamente estudou a abordagem básica de resolução de proble-
mas denominada dividir e conquistar. Utilizando essa abordagem, divide-se sucessivamente um problema em
subproblemas menores até que cada um deles possa ser resolvido por meio de um subprograma (i.e., função em
C). As soluções para os subproblemas são então combinadas de modo a resultar na solução para o problema ori-
ginal. Essa técnica de resolução de problemas também é conhecida como método de refinamentos sucessivos.
O raciocínio básico que deve ser utilizado na criação de funções recursivas é o mesmo que norteia a abordagem
de refinamentos sucessivos. A diferença diz respeito a como essa abordagem é usada em iteração e em recursão.
Na resolução de problemas usando iteração são criadas funções para resolver os subproblemas resultantes da
proposta da abordagem. No caso de recursão uma mesma função chamada recursivamente lida com os respec-
tivos subproblemas.
Para adaptar a abordagem de refinamentos sucessivos para recursão siga os seguintes passos:
1. Obtenha uma descrição precisa do problema que a função irá resolver, como sugerido na Seção 5.3.3.
Em especial, determine o tamanho do problema, que deverá ser representado por um ou mais parâ-
metros da função.
4.6 Como Pensar Recursivamente | 191
no mesmo intervalo de tempo quando o tamanho do array se tornar muito grande. Mas, à medida que o ta-
manho do array cresce, o espaço consumido pela versão recursiva se torna bem maior do que aquele utilizado
pela versão interativa por causa do correspondente aumento no número de registros de ativação necessários
para executar a função recursiva (v. Seção 4.3).
Comparando-se os protótipos das duas funções, nota-se que o parâmetro tam, que representa o tamanho do
array na versão iterativa, foi trocado pelos parâmetros inf e sup, que representam os índices inicial e final do
array na versão recursiva. Esses últimos parâmetros são realmente necessários para implementar uma função
recursiva com as mesmas especificações da função EmArray().
Agora, o protótipo da função EmArrayRec() deve parecer muito bizarro para qualquer programador de C
que exiba um mínimo de experiência. Afinal, por que utilizar como parâmetros os índices inicial e final de um
array quando o mais natural seria usar apenas o tamanho do array? Para ocultar essas esquisitices apresentadas
por algumas funções recursivas, como EmArrayRec(), costumam-se usar funções acionadoras. Uma função
acionadora funciona como intermediária e, tipicamente, contém apenas uma instrução, que é uma chamada
da função esquisita, como mostra a função EmArray2() a seguir.
int EmArray2(const int ar[], int tam, int num)
{
return EmArrayRec(ar, 0, tam - 1, num);
}
Quando usada adequadamente, recursão pode simplificar a solução de um problema, resultando em programas
mais curtos e mais fáceis de entender.
Não custa repetir que muitos problemas (talvez a maioria deles) apresentados como exemplos ou solicitados
como exercícios de programação neste e em outros livros de programação são melhor resolvidos sem o uso
de recursão. Até o presente ponto deste livro, apenas o problema das n rainhas é propício ao uso de recursão.
Então, a pergunta mais óbvia a ser levantada neste instante seria: se a maioria desses exemplos não deve ser seguida
na prática, por que eles aparecem com tanta frequência em textos de programação? Pode não parecer, mas a resposta
a essa questão também é óbvia: esses textos são dedicados ao ensino de programação e a principal motivação deles
é (ou deveria ser) didática. Mais precisamente, esses exemplos são escolhidos pela facilidade de entendimento
que eles apresentam.
A função Fatorial() apresentada abaixo é um dos exemplos favoritos em muitos textos de programação:
int Fatorial(int n)
{
if (!n)
return 1; /* Caso terminal */
return n*Fatorial(n - 1); /* Caso recursivo */
}
Essa função é frequentemente utilizada como primeiro exemplo de recursão em muitos textos porque ela na-
turalmente implementa a definição matemática de fatorial que muitas vezes é apresentada de forma recursiva:
n! =
{ 1
n.(n – 1)!
se n = 0
se n > 0
4.7 Quando Usar (e Não Usar) Recursão | 193
Comparando-se a implementação da função com a definição de fatorial acima, nota-se que a função realmente
reflete a definição matemática.
Acontece que o fatorial de um número também pode ser definido matematicamente sem o uso de recursão como:
n! =
{ 1,
1.2.3. ... .(n – 1).n
se n = 0
se n > 0
Agora, com um pouco de conhecimento de programação nota-se que essa última definição de fatorial é mais
propícia a ser implementada como uma função iterativa, como a função Fatorial2() abaixo:
int Fatorial2(int n)
{
int i, fat = 1;
if (n < 0)
return -1; /* Erro de domínio */
for (i = 1; i <= n; ++i)
fat *= i;
return fat;
}
A pergunta agora é: qual das duas funções que calculam fatorial é a melhor? Para responder essa pergunta as duas
funções precisam ser analisadas de acordo com três critérios:
Eficiência. Claramente, a função iterativa é mais eficiente do a versão recursiva, conforme já foi dis-
cutido acima.
Clareza (ou legibilidade). No presente caso, esse critério é um tanto subjetivo, pois ambas as imple-
mentações aparentam ser bastante fáceis de entender.
Funcionalidade. Aparentemente, a função iterativa leva vantagem sobre a função recursiva porque
existe a possibilidade de ocorrência de aborto de programa devido a esgotamento de pilha (stack over-
flow) se o parâmetro real recebido pela função recursiva for excessivamente grande. Mas, de fato, essa
aparente desvantagem da função recursiva é uma vantagem, pois se ocorrer aborto de programa, pelo
menos o programador notará que há algo de errado. O problema é que as duas funções padecem de
um mal maior: overflow de inteiro. Quer dizer, o fatorial de n resulta em valores muito grandes mes-
mo para valores relativamente pequenos de n. Por exemplo, se o tipo long long int, que é o maior
tipo inteiro primitivo de C, for utilizado em substituição ao tipo int em qualquer das duas funções
que calculam fatorial, o maior número para o qual o fatorial pode ser calculado sem ocorrência de
overflow de inteiro é 20.
A principal vantagem advinda do uso de recursão é que essa técnica pode reduzir consideravelmente o custo de
implementação de um programa. E, como o custo de recursos computacionais (i.e., tempo de processamento
e memória) tem cada vez mais diminuído, enquanto o custo de tempo de programação tem aumentado, vale
a pena considerar implementações recursivas.
A Tabela 4–1 a seguir compara iteração e recursão e pode servir como guia para decidir se o uso de recursão é
aceitável numa dada situação.
194 | Capítulo 4 — Recursão e Retrocesso
Iteração Recursão
Usa apenas laços de repetição Usa espaço adicional na pilha de execução
Recursão deve ser definitivamente removida de um programa se ela implica em processamento redundante,
como será visto na Seção 4.8.2. Às vezes, esse processamento redundante só é percebido com o uso de repre-
sentações gráficas de chamadas recursivas.
Alguns fabricantes de software adotam uma política de tolerância zero com respeito a recursão. Esses fabricantes
usam programas de análise estática de código para identificar chamadas recursivas e então elas são removidas
das versões finais dos programas.
4.8 Exemplos de Programação | 195
3
Haste A Haste B Haste C
Figura 4–23: Problema das Torres de Hanói
Problema: Escreva um programa que resolva o problema das torres de Hanói.
Solução: Considerando o diagrama da Figura 4–23, o problema consiste em deslocar os três discos na Haste A
(denominada de haste de origem) para a Haste C (denominada de haste de destino), utilizando a
Haste B como haste auxiliar. Por enquanto, não se preocupe com entrada e saída de dados do pro-
grama a ser desenvolvido e concentre-se na solução do problema para o caso geral no qual existem
n discos.
Para começar, suponha que a solução do problema para n – 1 discos seja conhecida. Então, se for
possível descrever a solução para n discos em termos da solução para n – 1 discos, o problema
será facilmente resolvido. De fato, isto é verdade porque, movendo-se n – 1 discos para a Haste B
(auxiliar) deixa-se apenas um disco para ser removido e, no caso trivial de um único disco, a solu-
ção é imediata: apenas mova esse disco da Haste A para a Haste C. Para um melhor entendimento,
considere novamente o caso particular onde n = 3 e acompanhe passo a passo a solução do proble-
ma mostrada na Figura 4–24.
Seguindo o raciocínio utilizado para resolver o problema das torres de Hanói com três discos, po-
de-se generalizar a solução para mover n discos da haste A para a haste C, utilizando a haste B como
auxiliar, por meio do algoritmo da Figura 4–25.
196 | Capítulo 4 — Recursão e Retrocesso
2 2
3 3 1
Haste A Haste B Haste C Haste A Haste B Haste C
1. Mova o disco 1 da haste A para a haste C 2. Mova o disco 2 da haste A para a haste B
3 2 1 3 2
Haste A Haste B Haste C Haste A Haste B Haste C
3. Mova o disco 1 da haste C para a haste B 4. Mova o disco 3 da haste A para a haste C
2 3 1 2 3
Haste A Haste B Haste C Haste A Haste B Haste C
5. Mova o disco 1 da haste B para a haste A 6. Mova o disco 2 da haste B para a haste C
2 2
1 3 3
Haste A Haste B Haste C Haste A Haste B Haste C
Exemplo de execução do programa: Executando o programa acima com um número de discos igual a
3 obtém-se:
Introduza o numero de discos: 3
Mova o disco 1 da haste A para a haste C
Mova o disco 2 da haste A para a haste B
Mova o disco 1 da haste C para a haste B
Mova o disco 3 da haste A para a haste C
Mova o disco 1 da haste B para a haste A
Mova o disco 2 da haste B para a haste C
Mova o disco 1 da haste A para a haste C
Uma questão que pode surgir na definição da função TorresDeHanoi() acima é: como os parâmetros desta
função são escolhidos? Parece trivial entender por que o número de discos deve ser um parâmetro que é reduzi-
do a cada chamada recursiva até que a condição de parada seja satisfeita. O uso das três hastes (hasteOrigem,
hasteDestino e hasteAuxiliar) como parâmetros também não é difícil de entender. Basta perceber que
soluções intermediárias do problema envolvem movimentos com as hastes A, B e C sendo ora origem, ora desti-
no, ora auxiliar do movimento. Deve-se observar ainda que o programa foi facilitado pelo fato de a numeração
dos discos ter sido feita do menor para o maior (verifique isto).
Observe que, da mesma forma que na elaboração da função SomaAteN2(), a solução obtida aqui para o problema
das torres de Hanói foi desenvolvida identificando-se um caso trivial (i.e., quando o número de discos é igual
a 1) e uma solução para o caso geral (i.e., para n discos) em termos de um caso mais simples (i.e., para n - 1
discos). Em termos de estratégia de resolução de problemas, entretanto, existe uma diferença fundamental entre
os dois problemas aqui descritos. O problema de encontrar a soma dos números entre 1 e n pode facilmente
ser resolvido sem o uso de recursão e a solução iterativa não apenas é mais clara como também é mais eficiente
em termos de recursos computacionais. O problema das torres de Hanói, por outro lado, não possui solução
não recursiva trivial e, portanto, representa uma situação prática na qual o uso de recursão é recomendável[2].
4.8.2 Fibonacci + Recursão = Ineficiência 1
Preâmbulo: Uma sequência de Fibonacci é uma sequência de números naturais, cujo primeiro termo é igual
a 0, o segundo termo é igual a 1 e cada número (exceto os dois primeiros) na sequência é igual
à soma de seus dois antecedentes mais próximos. Isto é, a sequência de Fibonacci é constituída
da seguinte forma:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
Problema: É relativamente fácil escrever uma função iterativa que gera números de Fibonacci (i.e., números
que fazem parte de uma sequência de Fibonacci). Mas é tentador escrever tal função recursiva-
mente, pois, afinal, um número de Fibonacci de ordem n pode ser definido recursivamente como:
Fib(n) =
{ n
Fib(n – 1) + Fib(n – 2)
se n < 2
se n ≥ 2
(a) Implemente uma função em C que reflita a definição recursiva apresentada acima. (b) Mostre
que essa implementação é deveras ineficiente.
Solução de (a):
[2] O quebra-cabeça das torres de Hanói foi inventado pelo matemático francês Edouard Lucas em 1883 e possui propriedades matemáticas
bastante curiosas. Por exemplo, utilizando o quebra-cabeça com 64 discos e movendo-se um disco por segundo, levar-se-iam 580 bilhões de
anos para completar a tarefa (v. Seção 6.11.6).
4.8 Exemplos de Programação | 199
int Fib(int n)
{
printf("Fib(%d)\t", n);
if (n < 2)
return n; /* Dois primeiros termos */
return Fib(n - 2) + Fib(n - 1); /* Caso recursivo */
}
Solução de (b): A Figura 4–26 mostra chamadas sucessivas da função Fib() quando ela é invocada num pro-
grama recebendo 6 como parâmetro real. Observe que, para calcular o número de Fibonacci
de ordem 6 são necessárias 25 chamadas da função Fib(). E o pior é que várias dessas cha-
madas são para efetuar o mesmo cálculo. Por exemplo, há duas chamadas Fib(4), três cha-
madas Fib(3) e cinco chamada Fib(0). Para n = 20, o número de chamadas cresce para
cerca de 22.000.
Fib(6)
Fib(5) Fib(4)
1 1 1 0
Fib(2) Fib(1) Fib(1) Fib(0) Fib(1) Fib(0) Fib(1) Fib(0)
1 1 0 1 0 1 0
Fib(1) Fib(0)
1 0
return atual;
}
A segunda maneira eficiente de calcular números de Fibonacci é usando raciocínio matemático. Quer dizer,
resolvendo a relação de recorrência que representa a sequência (v. Seção 6.9).
O exemplo chocante discutido nesta seção mostra que recursão deve ser cuidadosamente analisada antes que se
decida adotá-la. A função Fib() é elegante, mas extremamente ineficiente; por outro lado, a função iterativa
Fib2(), que é funcionalmente equivalente a Fib(), pode não ser tão elegante, mas é eficiente e relativamente
fácil de implementar.
4.8.3 Calculando o Comprimento de um String Recursivamente
Problema: Escreva uma função recursiva funcionalmente equivalente à função strlen() da biblioteca padrão
de C que calcula o comprimento de um string.
Solução: A função ComprimentoStrRec() definida abaixo calcula o comprimento de um string recursiva-
mente e retorna o resultado obtido.
int ComprimentoStrRec(const char *str)
{
if (!*str) /* O string é vazio */
return 0; /* Caso terminal */
/* Caso recursivo */
return 1 + ComprimentoStrRec(str + 1);
}
A função apresentada acima é relativamente fácil de entender. O caso terminal ocorre quando o string é vazio
(i.e., quando ele contém apenas o caractere terminal '\0'). O caso recursivo deve ser interpretado assim: o
comprimento de um string é um mais o comprimento do string sem seu primeiro caractere. Em termos de custo
temporal, tanto a função apresentada aqui quanto a função strlen() (que não é implementada recursivamente)
tem custo diretamente proporcional ao tamanho do string. Entretanto, essas duas funções diferem em termos
de custo de espaço utilizado: a função strlen() não usa espaço adicional (i.e., espaço além daquele necessário
para armazenar o próprio string); por outro lado, a função apresentada acima usa espaço adicional correspon-
dente aos registros de ativação alocados na pilha de execução durante as chamadas recursivas.
Observação: Este exemplo é meramente didático e não deve ser imitado na prática. Ou seja, a melhor maneira
de calcular o comprimento de strings é por meio de iteração.
4.8.4 Removendo Vogais de um String Recursivamente
Problema: Escreva uma função recursiva que remove todas as vogais de um string.
Solução:
void RemoveVogais(char *str)
{
static const char *const vogais = "aeiouAEIOU";
if (!*str) /* O string é vazio */
return; /* Caso terminal */
/* Casos recursivos */
if (strchr(vogais, *str)) /* Encontrada uma vogal */
RemoveVogais(strcpy(str, str + 1));
else
RemoveVogais(str + 1);
}
4.8 Exemplos de Programação | 201
A função RemoveVogais() é responsável pela remoção das vogais de um string e funciona do seguinte modo:
Novamente, caso terminal ocorre quando o string é vazio (v. Seção 4.8.3), pois, evidentemente, um
string vazio não possui nenhuma vogal a ser removida.
A função sob análise apresenta dois casos recursivos. O primeiro deles ocorre quando o primeiro ca-
ractere do string é uma vogal. Nesse caso, a porção do string que sucede essa vogal é copiada para a
porção inicial do string, assim, sobrescrevendo a vogal. Então, a função RemoveVogais() é chamada
recursivamente para remover as vogais desse novo string assim construído.
A segunda chamada recursiva refere-se à situação na qual nenhuma vogal foi encontrada no início
do string. Nesse caso, a função é chamada recursivamente para remover as vogais no string que resta
quando não se leva em consideração o primeiro caractere.
Observação: Este exemplo é meramente didático e não deve ser imitado na prática. Ou seja, a melhor maneira
de remover vogais de strings é por meio de iteração.
4.8.5 Exponenciação por Quadratura 1
{
Preâmbulo: A exponenciação de um número real x elevado a um número inteiro n > 0 pode ser definida
recursivamente como:
1 se n = 0
1 –n
x se n < 0
xn = n −1 2
x. x 2 se n for ímpar
n 2
x 2 se n for par
A função PreencheSudoku() chama EstaDisponivel() para verificar se um número pode ser usado no
quadrado especificado por uma linha e uma coluna de grade do quebra-cabeça Sudoku. Quando o referido
número estiver disponível para uso, essa última função retorna 1; caso contrário, ela retorna zero. Os parâme-
tros dessa função são:
grade[][] (entrada) que é o array bidimensional que representa o estado atual do quebra-cabeça
linha e coluna (entrada) representam a linha e a coluna que definem o referido quadrado
numero (entrada) é o número a ser verificado
int EstaDisponivel( int grade[][9], int linha, int coluna, int numero )
{
/* Linha inicial de uma subgrade (0, 3 ou 6) */
int linhaInicial = (linha/3) * 3,
/* Coluna inicial de uma subgrade (0, 3 ou 6) */
colunaInicial = (coluna/3) * 3;
for(int i = 0; i < 9; ++i) {
/* Verifica se o número já ocorre na linha em que o quadrado se encontra */
if (grade[linha][i] == numero)
return 0; /* O número já aparece na linha */
/* Verifica se o número já ocorre na coluna em que o quadrado se encontra */
if (grade[i][coluna] == numero)
return 0; /* O número já aparece na coluna */
/* Verifica se o número já ocorre na subgrade em que o quadrado se encontra */
if (grade[linhaInicial + (i%3)][colunaInicial + (i/3)] == numero)
return 0; /* O número já aparece na subgrade */
}
/* O número está disponível */
return 1;
}
A função main() apresentada a seguir define um array bidimensional que representa a grade de um quebra-ca-
beça Sudoku e chama a função PreencheSudoku() para resolver o problema.
int main(void)
{
/* Array que que representa a grade do quebra-cabeça. */
/* 0 significa um quadrado não preenchido. */
int grade[9][9]={ {8, 0, 0, 4, 0, 6, 0, 0, 7},
{0, 0, 0, 0, 0, 0, 4, 0, 0},
{0, 1, 0, 0, 0, 0, 6, 5, 0},
{5, 0, 9, 0, 3, 0, 7, 8, 0},
{0, 0, 0, 0, 7, 0, 0, 0, 0},
{0, 4, 8, 0, 2, 0, 1, 0, 3},
{0, 5, 2, 0, 0, 0, 0, 9, 0},
{0, 0, 1, 0, 0, 0, 0, 0, 0},
{3, 0, 0, 9, 0, 2, 0, 0, 5} };
/* Apresenta o desafio a ser enfrentado */
printf("\n>>> Quebra-cabeca a ser resolvido <<<\n");
ApresentaGrade(grade);
/* Verifica se o quebra-cabeça foi resolvido */
if(PreencheSudoku(grade, 0, 0)) { /* Foi resolvido */
/* Apresenta o resultado */
printf("\n>>> Quebra-cabeca resolvido <<<\n");
4.9 Exercícios de Revisão | 207
ApresentaGrade(grade);
} else { /* O quebra-cabeça não foi resolvido */
/* provavelmente porque foi mal formulado */
printf("\n\nEste Sudoku nao tem solucao\n\n");
return 1;
}
return 0;
}
A função main() acima chama ApresentaGrade(), definida abaixo, para apresentar na tela uma grade que
representa o quebra-cabeça Sudoku.
void ApresentaGrade(int grade[][9])
{
int i, j;
printf("\n+-----+-----+-----+\n");
for(i = 1; i < 10; ++i) {
for(j = 1; j < 10; ++j)
printf("|%d", grade[i - 1][j - 1]);
printf("|\n");
if (i%3 == 0)
printf("+-----+-----+-----+\n");
}
}
O ponto central do programa apresentado acima é a função PreencheSudoku() que, mesmo recheada de
comentários, não é fácil de entender por meio de uma simples leitura. Mas, assim como o funcionamento da
função Posiciona(), discutida na Seção 4.5.1, foi esclarecido por meio do uso de digramas que mostravam
diversos estados da pilha de execução, esse será o caso se você fizer o mesmo para a função PreencheSudoku().
#include <stdio.h>
int Funcao(int x)
{
int y = 0;
y += x;
return y;
}
(a) int main()
{
int a, contador;
for (contador = 1; contador <= 5; ++contador) {
a = Funcao(contador);
printf("%d ", a);
}
return 0;
}
#include <stdio.h>
int Funcao1(int a)
{
int b = 1;
b += 1;
return b + a;
}
int Funcao2(int x)
{
int b;
b = Funcao1(x);
(b) return b;
}
int main()
{
int a = 0, b = 1, contador;
for (contador = 1; contador <= 5; ++contador) {
b += Funcao1(a) + Funcao2(a);
printf("%d ", b);
}
return 0;
}
8. Considere a seguinte função
int F(int n, int m)
{
if (n > m)
return -1;
else if (n == m)
return 1;
else
return n*F(n+1, m);
}
(a) Qual é o caso base dessa função?
(b) Qual é o caso recursivo dessa função?
4.9 Exercícios de Revisão | 209
int F(int n)
{
if (n == 1)
return 2;
else
return 2*F(n - 1);
}
15. Qual é o valor retornado pela função F() a seguir quando é efetuada a chamada F(3, 2, 6)?
int F(int n, int a, int d)
{
if (n == 1)
return a;
else
return d + F(n - 1, a, d);
}
16. Qual é o valor retornado pela função F() a seguir quando é efetuada a chamada F(6, 8)?
int F(int i, int j)
{
if (j == i)
return i;
else if (j > i)
return F(i, j - i);
else
return F(i - j, j);
}
17. Considere a função Escreve() apresentada a seguir. (a) O que será escrito na tela quando for efetuada a
chamada Escreve(3)? (b) Quantas chamadas recursivas serão efetuadas nesse caso?
void Escreve(int n)
{
if (n > 0) {
Escreve(n - 1);
printf("%d", n);
Escreve(n - 1);
}
}
39. Como a abordagem de refinamentos sucessivos de construção de algoritmos deve ser adaptada para aco-
modar recursão?
40. Por que o protótipo de uma função recursiva parece às vezes bizarro? Apresente um exemplo.
41. O que é uma função acionadora de uma função recursiva?
Quando Usar (e Não Usar) Recursão (Seção 4.7)
42. Em que situações práticas devem ser utilizadas funções recursivas ao invés de iterativas (i.e., não recursivas)?
43. Qual é a melhor maneira de implementar uma função que calcula fatorial: iterativa ou recursiva? Explique
sua resposta.
44. Se a forma recursiva de implementação de fatorial não é recomendável, por que implementações recursivas
de fatorial aparecem tanto em livros de programação (inclusive este)?
Exemplos de Programação (Seção 4.8)
45. Se a função recursiva Fib() apresentada na Seção 4.8.2 for chamada recebendo como parâmetro real um
valor bem grande (digamos, maior do que 50), é provável que ocorrerá esgotamento de pilha? Explique
seu raciocínio.
46. Determine o número de adições na chamada Fib(10) da função Fib() apresentada na Seção 4.8.2.
47. (a) A função ExibeArquivoNaTelaRec() apresentada na Seção 4.8.7 é justificável? (b) A função
ExibeArquivoInvNaTelaRec() apresentada na Seção 4.8.7 é justificável?
48. Descreva o algoritmo de resolução do problema das torres de Hanói.
49. Descreva o método de cálculo de exponenciação por quadratura.
50. A seguinte função foi escrita como alternativa para a função ExponenciacaoQuad() apresentada na Seção
4.8.5. Explique por que esta função é menos eficiente do que a função ExponenciacaoQuad().
double ExponenciacaoAlt(double x, int y)
{
if (!y)
return 1;
if (y%2) /* Base é ímpar */
return x*ExponenciacaoAlt(x, y/2)*ExponenciacaoAlt(x, y/2);
else /* Base é par */
return ExponenciacaoAlt(x, y/2)*ExponenciacaoAlt(x, y/2);
}
51. Dentre os exemplos de funções recursivas apresentados neste capítulo, quais deles são justificáveis na prática?
{(
definição:
1 se k = 0 ou k = n
( )
n
k
=
n –1
) ( )
+
n
em caso contrário
k–1 k–1
(b) Desenhe uma árvore de recursão semelhante àquela apresentada na Seção 4.8.2 e discuta a ine-
ficiência da função solicitada.
4.10 Exercícios de Programação | 213
EP4.3 Escreva uma função recursiva em C, denominada Soma(), que avalie a soma de dois números inteiros
não negativos usando a função Sucessor() definida como:
int Sucessor(int x)
{
return ++x;
}
EP4.4 (a) Escreva uma função recursiva que calcule o máximo divisor comum de dois números inteiros po-
sitivos. (b) Escreva uma versão iterativa da função solicitada no item (a).
EP4.5 (a) Determine o que a seguinte função recursiva realiza:
int MinhaFuncao(int x)
{
if (!n)
return 0;
return (n + MinhaFuncao(n - 1));
}
(b) Escreva uma função iterativa que tenha o mesmo efeito da função anterior.
EP4.6 Escreva uma função recursiva que apresenta na tela um número inteiro não negativo em base binária.
EP4.7 Escreva uma função recursiva que converte strings numéricos (i.e., strings contendo apenas dígitos)
em números inteiros. Por exemplo, essa função transformaria o string "345" em 345.
EP4.8 Repita o exercício EP2.2 usando desta vez uma função recursiva para determinar se um número é
perfeito ou não.
EP4.9 Escreva uma função recursiva para calcular o número harmônico de ordem n definido como:
n
1
Hn = ∑
i =1 i
EP4.10 Modifique o programa para o problema das torres de Hanói apresentado na Seção 4.8.1 acima con-
siderando que os discos são numerados do maior para o menor.
EP4.11 Escreva uma função recursiva para determinar o maior valor de um array de elementos do tipo int.
EP4.12 Escreva uma função recursiva para determinar a média dos valores de um array de elementos do ti-
po int.
EP4.13 (a) Escreva uma função recursiva que remove de um string todas as ocorrências de um dado caractere.
O protótipo dessa função deve ser:
char *RemoveCaractere(char *str, int remover)
Nesse protótipo, str é o string que será eventualmente modificado, remover é o caractere a ser re-
movido e o retorno da função deve ser o endereço inicial do string. (b) Escreva um programa que lê
um string e um caractere via teclado e remove todas as ocorrências do caractere no string. O string
deve ser apresentado na tela antes e depois das eventuais substituições.
EP4.14 Implemente uma função recursiva, denominada ComparaStrings(), funcionalmente equivalente
à função strcmp().
EP4.15 Um algoritmo, denominado método de busca binária, para calcular a raiz cúbica de um número x
é o seguinte:
214 | Capítulo 4 — Recursão e Retrocesso
Algoritmo RaizCúbicaPorBuscaBinária
Entrada: Um número real x
Saída: A raiz de x
1. Comece com um limite inferior e outro superior
2. Se o número for maior do que 1, use 1 como limite inferior e o próprio número x como
limite superior
3. Se o número for menor do que 1, use x como limite inferior e 1 como limite superior
4. Se a diferença entre os limites inferior e superior é menor do que um certo valor de preci-
são (por exemplo, 0.000001), então o resultado será a média aritmética desses limites e o
problema estará resolvido
5. Se o problema ainda não estiver resolvido, verifique se a média dos limites inferior e supe-
rior é maior ou menor do que a raiz cúbica de x elevando esta média ao cubo
6. Se a média for menor do que a raiz cúbica de x, repita o processo a partir do passo 2 con-
siderando agora a média como limite inferior e mantendo inalterado o limite superior
7. Se a média for maior do que a raiz cúbica de x, repita o processo a partir do passo 2 man-
tendo o mesmo limite inferior e considerando a média como limite superior
(a) Implemente o algoritmo acima como uma função recursiva em C que recebe como parâmetros
um número de ponto-flutuante x e os limites inferior e superior descritos no algoritmo e retor-
na a raiz cúbica de x.
(b) Escreva um programa que solicita um valor numérico do usuário, calcula a raiz cúbica deste va-
lor utilizando a função descrita em (a) e imprime o resultado.
Sugestões:
(1) Defina a precisão como uma constante simbólica. Por exemplo:
#define PRECISAO 0.000001
(2) Utilize o seguinte protótipo para a função que calcula a raiz cúbica:
double RaizCubica(double x, double inferior, double superior)
EP4.16 Escreva uma função recursiva que retorna 1 quando um string possui apenas letras e dígitos ou 0, em
caso contrário. [Sugestão: Use a função isalnum() declarada em <ctype.h>.]
EP4.17 Escreva uma função recursiva que substitui cada caractere de tabulação de um string por um espaço
em branco.
EP4.18 Escreva uma função recursiva, denominada OcorrenciasCar(), que conta o número de ocorrências
de um caractere num string.
EP4.19 Escreva uma função recursiva que recebe um array bidimensional N x N como parâmetro e calcula a
soma dos elementos do array.
EP4.20 Apesar de não apresentar recursão de cauda, a função ExponenciacaoQuad() apresentada na
Seção 4.8.5 pode ser facilmente implementada sem o uso de recursão. Reimplemente a função
ExponenciacaoQuad() de modo iterativo.