Conteúdo principal
Ciência da Computação
Curso: Ciência da Computação > Unidade 1
Lição 6: Algoritmos recursivos- Recursividade
- A função fatorial
- Desafio: Fatorial iterativo
- Fatorial recursivo
- Desafio: Fatorial recursivo
- Propriedades de algoritmos recursivos
- Usando recursividade para determinar se uma palavra é um palíndromo
- Desafio: A string é um palíndromo?
- Calculando a potência de um número
- Desafio: Potência recursiva
- Recursão múltipla com o triângulo de Sierpinski
- Melhorando a eficiência das funções recursivas
- Projeto: Arte recursiva
© 2023 Khan AcademyTermos de usoPolítica de privacidadeAviso de cookies
Melhorando a eficiência das funções recursivas
A recursão pode ser uma maneira elegante de resolver um problema, e muitos algoritmos se prestam a soluções recursivas. No entanto, algoritmos recursivos podem ser ineficientes em termos de tempo e espaço. Aqui, vamos explorar várias técnicas para melhorar sua eficiência.
No desafio de programação para computar recursivamente o fatorial de um número, pedimos para você chamar a função várias vezes com valores diferentes.
Por exemplo, este código JavaScript chama
factorial()
4 vezes:factorial(0);
factorial(2);
factorial(5);
factorial(10);
Vamos considerar todas as chamadas que o computador faz ao executar essas 4 linhas de código:
Linha de código | Chamadas recursivas | Total de chamadas |
---|---|---|
factorial(0) | 1 chamada | |
factorial(2) | factorial(1) → factorial(0) | 3 chamadas |
factorial(5) | factorial(4) → factorial(3) → factorial(2) → factorial(1) → factorial(0) | 6 chamadas |
factorial(10) | factorial(9) → factorial(8) → factorial(7) → factorial(6) → factorial(5) → factorial(4) → factorial(3) → factorial(2) → factorial(1) → factorial(0) | 11 chamadas |
Observe que
factorial(10)
tem que fazer 11 chamadas de função, e 6 delas têm exatamente os mesmos argumentos e valores de retorno que as chamadas de função anteriores feitas durante factorial(5)
.Memoização do fatorial
Podemos usar uma técnica chamada memoização para economizar tempo do computador ao fazer chamadas de funções idênticas. A memoização (uma forma de armazenamento em cache) lembra o resultado de uma chamada de função com entradas específicas em uma tabela de busca (o "memorando") e retorna esse resultado quando a função é chamada novamente com as mesmas entradas.
Uma memoização da função fatorial pode ser assim:
- Se n = 0, retorne 1
- Caso contrário, se n estiver no memorando, retorne o valor do memorando para n
- Caso contrário,
- Calcule left parenthesis, n, minus, 1, right parenthesis, !, times, n
- Armazene o resultado no memorando
- Retorne o resultado
Esse algoritmo verifica o valor de entrada no memorando antes de fazer uma chamada recursiva potencialmente cara. O memorando deve ser uma estrutura de dados com tempos de busca eficientes, como uma tabela de mapeamento com tempo de busca O, left parenthesis, 1, right parenthesis.
Se você quiser visualizar a execução do algoritmo memoizado implementado em JavaScript, assista a este vídeo ou execute a visualização você mesmo. Antes de assistir, você pode se desafiar a implementar o algoritmo na linguagem de sua escolha.
Com a memoização implementada, o computador pode fazer menos chamadas totais sobre chamadas repetidas para
factorial()
:Linha de código | Chamadas recursivas | Total de chamadas |
---|---|---|
factorial(0) | 1 chamada | |
factorial(2) | factorial(1) → factorial(0) | 3 chamadas |
factorial(5) | factorial(4) → factorial(3) → factorial(2) | 3 chamadas |
factorial(10) | factorial(9) → factorial(8) → factorial(7) → factorial(6) → factorial(5) | 6 chamadas |
A memoização faz uma troca entre tempo e espaço. Contanto que a busca seja eficiente e a função seja chamada repetidamente, o computador pode economizar tempo com o custo de usar memória para armazenar o memorando.
Memoização de Fibonacci
No caso da função fatorial, um algoritmo só se beneficia da otimização da memoização quando um programa faz repetidas chamadas à função durante sua execução. Em alguns casos, no entanto, a memoização pode economizar tempo até para uma única chamada para uma função recursiva.
Vejamos um exemplo simples, o algoritmo para gerar números de Fibonacci.
A sequência de Fibonacci é uma série de números famosa em que o próximo número na sequência é a soma dos 2 números anteriores. Os dois primeiros números na sequência são definidos como 0 e 1. Depois disso, o próximo número é 1 (de 0, plus, 1) e o número depois disso é 2 (de 1, plus, 1), e assim por diante.
Os primeiros 10 números de Fibonacci:
Uma função recursiva simples para gerar o n-ésimo número de Fibonacci se parece com isto:
- Se n é 0 ou 1, retorne n
- Caso contrário, retorne start text, f, i, b, o, n, a, c, c, i, end text, left parenthesis, n, minus, 1, right parenthesis, plus, start text, f, i, b, o, n, a, c, c, i, end text, left parenthesis, n, minus, 2, right parenthesis
Esse algoritmo é um exemplo de recursão múltipla, pois chama a si mesmo várias vezes. Para entender as várias chamadas que o computador faz, é importante visualizar as chamadas como uma árvore.
Quando n, equals, 5, o computador faz 15 chamadas:
Observe as múltiplas chamadas para a função fibonacci para os valores de entrada de 3, 2, 1 e 0. À medida que a entrada aumenta, isso se torna cada vez mais ineficiente. Uma chamada para
fibonacci(30)
resulta em uma chamada fibonacci(2)
feita pelo computador mais de meio milhão de vezes.Podemos usar a memoização aqui para evitar que o computador recalcule um número de Fibonacci que já foi calculado.
A versão memoizada do algoritmo recursivo de Fibonacci se parece com isto:
- Se n for 0 ou 1, retorne n
- Caso contrário, se n estiver no memorando, retorne o valor do memorando para n
- Caso contrário,
- Calcule start text, f, i, b, o, n, a, c, c, i, end text, left parenthesis, n, minus, 1, right parenthesis, plus, start text, f, i, b, o, n, a, c, c, i, end text, left parenthesis, n, minus, 2, right parenthesis
- Armazene o resultado no memorando
- Retorne o resultado
Se você quiser visualizar a execução do algoritmo memoizado implementado em JavaScript, assista a este vídeo ou execute a visualização você mesmo.
Para n, equals, 5, o computador faz 9 chamadas:
A versão original do algoritmo exigia 15 chamadas de função, então a memoização eliminou 6 chamadas de função.
Esta tabela mostra o número de chamadas necessárias de n, equals, 5 até n, equals, 10:
n | Original | Memoizado |
---|---|---|
5 | 15 | 9 |
6 | 25 | 11 |
7 | 41 | 13 |
8 | 67 | 15 |
9 | 109 | 17 |
10 | 177 | 19 |
O número total de chamadas de função aumenta a uma taxa exponencial para o algoritmo original, mas a uma taxa linear muito mais lenta para o algoritmo memoizado.
O algoritmo memoizado requer mais espaço, no entanto; o suficiente para que o memorando armazene cada valor de retorno de n.
Abordagem de baixo para cima
Às vezes, a melhor maneira de aprimorar a eficiência de um algoritmo recursivo é não usar a recursão, ou recursividade.
No caso da geração de números de Fibonacci, uma técnica iterativa chamada abordagem de baixo para cima pode economizar tempo e espaço. Ao usar uma abordagem de baixo para cima, o computador encontra primeiro os subproblemas e usa os resultados parciais para chegar ao resultado final.
Uma abordagem de baixo para cima para a geração de números de Fibonacci se parece com isto:
- Se n for 0 ou 1, retorne n
- Caso contrário,
- Crie a variável start text, d, o, i, s, A, t, r, a, with, \', on top, s, end text para lembrar o resultado de left parenthesis, n, minus, 2, right parenthesis e inicie com 0
- Crie a variável start text, u, m, A, t, r, a, with, \', on top, s, end text para lembrar o resultado de left parenthesis, n, minus, 1, right parenthesis e inicie com 1
- Crie a variável start text, r, e, s, u, l, t, a, d, o, end text para armazenar o resultado final e inicie com 0
- Repita left parenthesis, n, minus, 1, right parenthesis vezes:
- Atualize start text, r, e, s, u, l, t, a, d, o, end text para a soma de start text, d, o, i, s, A, t, r, a, with, \', on top, s, end text mais start text, u, m, A, t, r, a, with, \', on top, s, end text
- Atualize start text, d, o, i, s, A, t, r, a, with, \', on top, s, end text para armazenar o valor atual de start text, u, m, A, t, r, a, with, \', on top, s, end text
- Atualize start text, u, m, A, t, r, a, with, \', on top, s, end text para armazenar o valor atual de start text, r, e, s, u, l, t, a, d, o, end text
- Retorne start text, r, e, s, u, l, t, a, d, o, end text
Essa abordagem nunca faz uma chamada recursiva; em vez disso, usa a iteração para somar os resultados parciais e calcular o número.
Se você quiser visualizar a execução do algoritmo de baixo para cima implementado em JavaScript, assista a este vídeo ou execute a visualização você mesmo.
O algoritmo de baixo para cima tem a mesma complexidade de tempo O, left parenthesis, n, right parenthesis que o algoritmo memoizado, mas requer apenas o espaço O, left parenthesis, 1, right parenthesis, já que só guarda na memória três números por vez.
Programação dinâmica
Memoização e abordagem de baixo para cima são técnicas da programação dinâmica, uma estratégia de resolução de problemas usada em matemática e ciência da computação.
A programação dinâmica pode ser usada quando um problema tem subestrutura ótima e subproblemas sobrepostos. Subestrutura ótima significa que a solução ótima para o problema pode ser criada a partir de soluções ótimas de seus subproblemas. Em outras palavras,
fib(5)
pode ser calculado com fib(4)
e fib(3)
. Subproblemas sobrepostos acontecem sempre que um subproblema é resolvido várias vezes, o que vimos quando fib(5)
fez várias chamadas para fib(3)
e fib(2)
tipicamente recursivos.A programação dinâmica pode ser usada para uma variedade de problemas e envolve técnicas além das que aprendemos aqui. Se você estiver trabalhando em um problema com subestrutura ótima e subproblemas sobrepostos, considere se uma abordagem de programação dinâmica pode funcionar.
Quer participar da conversa?
Nenhuma postagem por enquanto.