// Write code that computes the Fibonacci sequance upto n
#include <cstdint>
#include <vector>
#include <iostream>
#include <functional>
#include <chrono>
// Recursive implementations of Fibonacci are all the rage in a teaching
// environment as it demonstrates recursion nicely. The problem is that a recursive
// implementation is completely a inefficient way to code the task in practice.
// Each call of function calls 2 more and those 2 call 2 more each resulting in 4.
// And those 4 call 4 more each resulting in 8 so at this point your space and time
// complexity is 1 + 2 + 4 + 8 + ... which is exponential growth and just awful
uint32_t fib_naive(uint32_t n)
{
if (n == 0) return 0;
if (n == 1) return 1;
return fib_naive(n-1) + fib_naive(n-2);
}
// So what google is really testing here is the "dynamic programming" paradigm.
// https://en.wikipedia.org/wiki/Dynamic_programming. Unfortunately the term
// "dynamic programming" has had its meaning diluted and corrupted a bit. In
// this context what it means is the breaking of a larger problem into sub
// parts and solving each sub part to solve the whole. This sounds exactly
// like recursion and it is, the distinction is that recursive implementations
// tend to stop at raw mathematical conversion where as DP implementations look
// at lazy caching results to cut off repeated computations
// There are two textbook approaches to this; top-down and bottom-up
// in the top-down approach you start working from the answer point and compute
// the values of the breakdowns, you deploy memory as needed to cache the
// results of prior computations so that if they are used again you use the
// cached value rather than recomputing it from scratch again
uint32_t fib_top_core(uint32_t n, std::vector<uint32_t>& cache)
{
if (n > 0 and cache[n] == 0)
{
cache[n] = fib_top_core(n-2,cache) + fib_top_core(n-1,cache);
}
return cache[n];
}
uint32_t fib_top(uint32_t n)
{
if (n == 0) return 0;
std::vector<uint32_t> cache(n+1, 0);
cache[1] = 1;
return fib_top_core(n, cache);
}
// in the bottom-up approach you start working from the known terminators
// and work your way up to the answer point. You do this by computing
// the values of all the breakdowns you could possibly need and memorize
// these the results for use in the next computations. Often you can optimize
// the memory to forget values that we know we will not revisit
uint32_t fib_bot(uint32_t n)
{
if (n == 0) return 0;
uint32_t fib_minus_2 = 0;
uint32_t fib_minus_1 = 0;
uint32_t fib_current = 1;
while (n > 1)
{
fib_minus_2 = fib_minus_1;
fib_minus_1 = fib_current;
fib_current = fib_minus_2 + fib_minus_1;
--n;
}
return fib_current;
}
void test(std::function<uint32_t(uint32_t)> func, uint32_t n)
{
auto start = std::chrono::system_clock::now();
std::cout << "result:" << func(n);
auto end = std::chrono::system_clock::now();
std::cout << " -- time:"
<< std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count()
<< "nSec\n";
}
int main()
{
int n = 40;
test(fib_naive,n);
test(fib_top ,n);
test(fib_bot ,n);
}
and the output looks like:
result:102334155 -- time:1282786361 nSec result:102334155 -- time:51012 nSec result:102334155 -- time:690 nSec
No comments:
Post a Comment