Recursão
Aula relacionada recomendada
Introdução
Recursão é definir algo em termos de si próprio. Um dos exemplos mais clássicos para ilustrar esse conceito é a sequência de Fibonacci. Ela é uma sequência de números inteiros de tal forma que os primeiros elementos são \(0\) e \(1\) e os próximos elementos são definidos como a soma dos dois anteriores.
Em linguagem matématica, definimos o \(n\)-ésimo termo de Fibonacci como:
No nosso contexto, vamos sempre estar trabalhando com funções recursivas, e considerando a sequência de Fibonacci, podemos escrever uma função recursiva que retorna o \(n\)-ésimo número de Fibonacci. Em código:
fibonacci.cpp | |
---|---|
Observação
Cada chamada recursiva vai criar um novo "ambiente", as variáveis e argumentos são diferentes para cada chamada da função recursiva. Assim quando uma função recursiva se chama, apesar dos nomes das variáveis serem os mesmos, os valores em cada váriavel são independentes entre si. Como se fossem várias caixas com o mesmo nome, mas com conteúdos distintos.
Podemos perceber que a definição matemática do termo de Fibonacci e a função que fizemos são muito similares. Primeiramente, definimos o que chamamos de casos bases, que são situações em que não definimos a função em termos de si mesma, na sequência de Fibonacci, os casos bases seriam quando \(n\) é igual a \(0\) ou \(1\). Depois, definimos as relações de recorrência, quando não estamos tratando os casos básicos. Nelas, a função recursiva é definida em termo dela mesma, e como ela se relaciona consigo mesma é o que chamaremos de relação de recorrência. Uma função pode ter mais de uma relação de recorrência.
Usando a recursão, podemos resolver muitos problemas com bastante facilidade. A seguir, veremos exemplos de como utilizar a recursividade.
Recomendação
Nos exemplos, execute os códigos e tente simular o fluxo de execução da função recursiva para diferentes valores. Para cada chamada da função, tente escrever os valores das variáveis e argumentos para ajudar na compreensão.
Exemplos:
Contagem regressiva
Vamos definir uma função que imprime os números de \(N\) até \(1\) de maneira decrescente (uma contagem regressiva) usando a recursividade. Primeiro analisamos os possiveis casos base. Neste exemplo, temos um caso base, quando \(n = 1\). Quando \(n = 1\), vamos imprimir o número \(1\) e encerrar a contagem. Agora definimos as relações de recorrência, se \(n > 1\), então imprimimos o número \(n\) na tela e continuamos a contagem a partir do número \(n-1\). Ou seja:
Em código:
ContagemRegressiva(N)
que ela irá realizar a contagem regressiva de \(N\) até \(1\).
Recomendação
Sempre tente escrever a função recursiva no papel, você não precisa definir ela de maneira completamente matemática, mas escrever no papel como a função recursiva funciona vai te ajudar a implementá-la.
Contagem progressiva
Agora vamos definir uma função que imprime os números de \(1\) até \(N\) de maneira crescente (uma contagem progressiva). Pensando no exemplo anterior, poderíamos considerar o caso base \(n = N\) em que imprimos \(n\) e encerramos a contagem. A relação de recorrência seria quando \(n < N\), nesse caso imprimimos \(n\) e continuamos a contagem a partir do número \(n+1\). Ou seja:
Porém, existe outra maneira de definir essa função de tal maneira que não precisamos desse segundo parâmetro \(N\). Considerando:
Em código:
ContagemProgressiva(N)
sem precisar usar dois parâmetros. Essa implementação parece muito similar a da ContagemRegressiva(n)
, com exceção de que a impressão agora é feita depois da chamada recursiva, esse detalhe que faz a diferença. Quando chamamos a função recursiva primeiro, vamos chamar a função recursivamente até alcançarmos o caso base \(n = 1\). Somento quando \(n = 1\), vamos imprimir o número \(1\) e retornar das funções mais internas e imprimir os próximos números em ordem crescente.
Subconjuntos
Dado um conjunto \(S\) de números naturais, vamos criar um programa que imprime todos os subconjuntos de \(S\). Para fazer isso, inicialmente, vamos ter um conjunto \(T\) vazio que usaremos para construir todos os subconjuntos possíveis. Considere que \(S\) tem \(N\) elementos. Para cada elemento, podemos escolher incluí-lo em \(T\) ou não. Cada escolha gera um novo subconjunto. Vamos pensar em uma definição recursiva com essa ideia em mente, para cada elemento \(x\) em \(S\), temos duas opções:
- incluir \(x\) em \(T\) e gerar o conjunto \(T \cup \{x\}\) ou
- não incluir \(x\) em \(T\) e manter o mesmo conjunto \(T\).
Após fazer a escolha para \(x\), podemos desconsiderar \(x\) em \(S\) e passar a considerar o conjunto \(S - \{x\}\). Com essa ideia, podemos implementar a função recursiva.
Em código:
vector
como argumento, isso vai alocar espaço na memória a cada chamada e atrasar o nosso programa. Para evitar isso vamos considerar \(S\) e \(T\) vector
's globais e vamos utilizar índices para considerar os elementos de S e também o caso base.
Subconjuntos(0)
.
Recomendação
Se você tem que alterar uma estrutura mais complicada em uma função vector
, set
ou map
por exemplo. Tente colocar essas estruturas globalmente, assim você tem menos chance de sem querer passar essas estruturas como argumento e alocar memória desnecessariamente.