No último artigo (Fibonacci, Integer overflow, memoization e um exagero), eu mostrei um código Fibonacci que era bem lento. Fui otimizando ele usando memoization e também mostrando uma outra implementação com loops. Agora vou detalhar mais e mostrar algumas soluções alternativas que aprendi nesse meio tempo.
Pra facilitar, usarei a notação Big O pra explicar como é a performance de cada implementação.
1. O(2^N) O clássico, mas lento.
Essa é uma implementação comum. Funciona? Claro! Mas se liga como fica a nossa pilha de chamadas caso quisermos o número na posição 5 da sequência:
Percebe que cada chamada gera mais duas? E pior, algumas delas, como fib(3)
e fib(2)
são repetidas várias vezes, recalculando tudo de novo.
No total, são 11 passos pra encontrar o número na posição 5 da sequência de Fibonacci, há um crescimento exponencial: cada chamada da função inicia mais uma ou duas chamadas. Assim, a complexidade do algoritmo tem como pior caso O(2^N).
Se quisermos ser precisos, na verdade, o crescimento segue a proporção áurea (≈1,618), sendo então O(1,618^N).
Agora imagina o impacto:
- Se N = 10 → 122 passos.
- N = 30 → MAIS DE 1 MILHÃO DE PASSOS!!
Com memoization podemos evitar cálculos repetidos. Assim, o algoritmo se torna O(N) em tempo, mas com um custo de memória adicional (pelo cache).
Mas dá pra gente resolver o mesmo problema sem consumir mais memória.
2. O(N) Tail recursion
Podemos adicionar dois novos parâmetros à função (os dois últimos números da sequência atual). Assim, não há recálculo e a complexidade continua O(N), mas sem custo extra de memória:
Aqui a pilha de chamadas para N = 5
Sem memoization, sem crescimento exponencial… perfecto! Percebe como as recursões mudaram bastante? A primeira era uma recursão binária (cada chamada gerava mais duas, um crescimento exponencial), enquanto essa é uma tail recursion: a chamada recursiva ocorre como a última operação da função, de modo que o resultado da função não depende mais de cálculos adicionais após a chamada recursiva.
3. O(N) com Dynamic Programming
Sem recursão, mas com o mesmo raciocínio da solução anterior. Nada de cálculos desnecessários, criamos 2 variavéis para os 2 últimos valores calculados e seguimos com a sequência.
4. O(1) com a formula de Binet
Usando a fórmula de Binet, podemos calcular diretamente, mas isso só funciona enquanto n < 71
, por culpa dos problemas de imprecisão nos cálculos com ponto flutuante.
Sem loops ou recursão, só copiando e colando uma fórmula.
Referências & Inspirações
https://craftofcoding.wordpress.com/2021/12/10/fibonacci-by-linear-recursion-is-better/ (thx @geckones)
https://artofproblemsolving.com/wiki/index.php/Binet%27s_Formula