🦸♂️ Coding Interview Hero 🦸♀️
Fibonacci suite
Exercice: compute nth element of Fibonacci suite using a recursive function. F0 = F1 = 1 and Fn+2 = Fn+1 + Fn
You can assume n is positive.
def fibonacci(n: Int): BigInt = n match {
case 0 | 1 => n
case _ => Fibonacci(n - 2) + Fibonacci(n - 1)
}
Question: what is the time complexity? $$ T_n = T_{n-1} + T_{n-2} + O(1) $$
We can approximate lower and upper bounds: $$ O(2^{\frac{n}{2}}) \leq T_n \leq O(2^n) $$
And if you are good at maths (useful inputs here), you can even prove that: $$ T_n = O(\phi^n) $$
This is really poor (exponential complexity) and on an average laptop it will take several seconds just to compute the 20th element. To be more concrete, let's use JMH (why you should always use JMH) to run a benchmark:
package fibonacci
import org.openjdk.jmh.annotations.{Benchmark, Fork, Measurement, Warmup}
import org.openjdk.jmh.infra.Blackhole
class FibonacciBenchmark {
def fibonacci(n: Int): BigInt = n match {
case 0 | 1 => n
case _ => fibonacci(n - 2) + fibonacci(n - 1)
}
def iterativeFibonacci(n: Int): BigInt = {
var a: BigInt = 0
var b: BigInt = 1
var i = 0
while (i < n) {
val tmp: BigInt = a + b
a = b
b = tmp
i = i + 1
}
a
}
@Benchmark
@Warmup(iterations = 2)
@Fork(value = 2)
@Measurement(iterations = 2)
def recursive(blackHole: Blackhole): BigInt = {
val fib20 = fibonacci(20)
blackHole.consume(fib20)
fib20
}
@Benchmark
@Warmup(iterations = 2)
@Fork(value = 2)
@Measurement(iterations = 2)
def iterative(blackHole: Blackhole): BigInt = {
val fib20 = iterativeFibonacci(20)
blackHole.consume(fib20)
fib20
}
}
Then running sbt run jmh:run
will produce such output:
Benchmark |
Mode |
Threads |
Samples |
Score |
Score Error (99,9%) |
Unit |
fibonacci.FibonacciBenchmark.recursive |
thrpt |
1 |
4 |
5962,057514 |
557,707975 |
ops/s |
fibonacci.FibonacciBenchmark.iterative |
thrpt |
1 |
4 |
3703413,835 |
308887,785 |
ops/s |
Exercice: use the tail recursion to improve the performances.
@tailrec
private def tailRecFibonacci(n: Int, a: BigInt = 0, b: BigInt = 1) : BigInt = n match {
case 0 => a
case 1 => b
case _ => tailRecFibonacci(n - 1, b, a + b)
}
Question: what is the time complexity? $$ T_n = O(n) $$
Let's add a benchmark and compare the results:
Benchmark |
Mode |
Threads |
Samples |
Score |
Score Error (99,9%) |
Unit |
fibonacci.FibonacciBenchmark.recursive |
thrpt |
1 |
4 |
5962,057514 |
557,707975 |
ops/s |
fibonacci.FibonacciBenchmark.tailRecursive |
thrpt |
1 |
4 |
4008972,107 |
104654,183 |
ops/s |
fibonacci.FibonacciBenchmark.iterative |
thrpt |
1 |
4 |
3703413,835 |
308887,785 |
ops/s |
Question: does the @tailrec
annotation change the performaces?
Answer: no. It's only used by the compiler to verify for you if your function is tail recursive. If not, you will have a compile error. You can check the benchmark on github
All code is available on github.