Search⌘ K
AI Features

Calculating Fibonacci Numbers

Explore how to calculate Fibonacci numbers by understanding the classic recursive approach and its inefficiencies. Learn to analyze time complexity through recurrence relations, identify redundant calculations, and apply memoization to optimize the algorithm for better performance in C#.

Classic recursive implementation of the Fibonacci series

Before we dive into what dynamic programming is, let’s have a look at a classic programming problem: the Fibonacci series. You’ve probably already seen it, but let’s start with a quick refresher. The Fibonacci series is a series of numbers in which each number is the sum of the preceding two numbers. The first two numbers are 0 and 1. So, it looks like 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

Here is a C# method that returns the nthn^{th} Fibonacci number.

This is the Fibonacci golden spiral. The width of each square represents a Fibonacci number.

C#
using System;
class Program
{
/// <summary>
/// Finds the nth Fibonacci number.
/// </summary>
/// <param name="num">An integer number.</param>
/// <returns>The nth Fibonacci number.</returns>
public static int Fib(int num)
{
if (num <= 1)
{
return num;
}
return Fib(num - 1) + Fib(num - 2);
}
// Driver code to test above method
public static void Main(string[] args)
{
var num = 6;
Console.WriteLine(Fib(num)); // Output: 8
}
}

Time complexity with recurrence relations

To calculate the time complexity of the code, we can solve a recurrence relation,

{T(0)=0,T(1)=1,T(n)=T(n1)+T(n2)+4\begin{cases} T(0) = 0, \\ T(1) = 1, \\ T(n) = T(n-1)+T(n-2)+4 \end{cases}

T(n)T(n) represents the Fib method. T(n1)T(n-1) and T(n2)T(n-2) represent the recursive calls to Fib(n-1) Fib(n-2), whereas the 44 represents the basic operations in the code. Namely, the comparison on line 12 and the two subtractions and one addition on line 17. Lastly, T(1)=1T(1) = 1 and T(0)=1T(0) = 1 represent the base cases.

To solve this recurrence, we can simplify it by approximating that T(n1)T(n2)T(n-1) \approx T(n-2). The time that is taken to calculate T(n1)T(n-1) is greater than the time taken to calculate T(n2)T(n-2), so this approximation gives us an upper bound on the total time complexity,

Eq 1: T(n)=2T(n1)+4T(n) = 2T(n-1)+4.

Also, by this relation,

Eq 2: T(n1)=2T(n2)+cT(n-1) = 2T(n-2)+c, where c=4c = 4.

So, we can replace T(n1)T(n-1) in eq 1 with its value in eq 2 like so to get,

T(n)=2(2T(n2)+c)+cT(n) = 2(2T(n-2)+c)+c

=4T(n2)+3c= 4T(n-2)+3c

=4(2T(n3)+c)+3c= 4(2T(n-3)+c)+3c

=8T(n3)+7c= 8T(n-3)+7c

\cdots

You must have noticed a pattern here, which is that this equation can be written in generic terms.

Generic: T(n)=2kT(nk)+(2k1)c T(n) = 2^kT(n-k)+(2^k-1)c \space

Here, kk is the number of times that the equation was substituted into plus one. Have a look at the following for a clearer picture:

T(n)=2T(n1)+c            k=1T(n) = 2T(n-1)+c \;\;\;\;\;\; \color{red} k \color{red}= \color{red}1

T(n)=4T(n2)+3c        k=2T(n) = 4T(n-2)+3c \;\;\;\; \color{red} k \color{red}= \color{red}2

T(n)=8T(n3)+7c        k=3T(n) = 8T(n-3)+7c \;\;\;\; \color{red} k \color{red}= \color{red}3

\cdots

Let’s consider what value of kk brings the argument of T(nk)T(n-k) to the base case of 1. If nk=1n - k = 1, then k=n1k = n - 1. Next, we substitute k=n1k = n - 1 in the expression for T(n)T(n). We can now put the generic equation in terms of T(1)T(1), the value of which we know is 11:

T(n)=2n1+(2n11)cT(n) = 2^{n-1}+(2^{n-1}-1)c

=2n1+c2n1c= 2^{n-1}+ c2^{n-1}-c

=2n1(1+c)c = 2^{n-1}(1+ c)-c \space

Hence,

T(n)=2n1(1+c)cT(n) = 2^{n-1}(1+ c)-c

This means that the Fib(n) method is in O(2n)O(2^n), i.e., exponential time, which isn’t very efficient. What is causing this inefficiency and how can we improve this? Let’s take a look at that now.

Redundant calculations in classic Fibonacci

One factor is the fact that some terms of Fib(n) are calculated several times. Look at the following slides to see which ones are repeated in the case of Fib(6):

canvasAnimation-image
1 / 25

How can we improve this? Well, it turns out that these redundant calculations can be eliminated, which would significantly reduce the algorithm’s time complexity using a technique called memoization.