We will introduce some new Julia functions. In particular we will see how to use recursions and loops through a small example.
First we will warm up your Julia knowledge by showing you how to compute $\sum_{i=1}^{n} i$. There is a well known formula due to Gauss that reads: (more details in https://en.wikipedia.org/wiki/1_%2B_2_%2B_3_%2B_4_%2B_%E2%8B%AF)
\begin{equation} \sum_{i=1}^{n} i = \frac{n(n+1)}{2}. \end{equation}The proof normally needs recursion; however, we will do a proof "by intimidation". We will write a small function that computes the former and test it against the formula, and see that they match even for large $n$.
One extremely easy although suboptimal way of computing $\sum_{i=1}^{n} i$ is to write
In [ ]:
n = 11
sum([1:n])
This is suboptimal because it needs to allocate the vector $[1:n]$, which is $[1 ,2 ,3 ,4..., n-1, n]$, and then add all the components. This means that we need $\cal{O}(n)$ memory. You can see this problem by running the macro @time, which computes the time and the memory allocation of the function. You can run the snippet below for different $n$ and you should observe that more bytes are being allocated as $n$ increases (the number whoud grow linearly with $n$).
In [ ]:
n = 100
@time sum([1:n])
We can perform the sum using a loop only requiring $\cal{O}(1)$ memory.
In [ ]:
function sumLoop(n)
result = 0.0
for i = 1:n
result = result + i
end
return result
end
You can easily test that the two functions provide the same result by running
In [ ]:
sumLoop(n) - sum(1:n)
Now you can see the memory footprint of the new function by running
In [ ]:
n = 100
@time sumLoop(n)
You can increase $n$ as much as you want and the memory allocated will be independent of $n$. Now that we have a good algorithm to compute $\sum_{i=1}^{n} i$ we will test if the formula is correct by running
In [ ]:
n = 100000000
resultFromLoop = sumLoop(n)
resultFromFormula = n*(n+1)/2
print("Result from Loop = ",resultFromLoop, "\n" )
print("Result from Formula = ",resultFromFormula, "\n" )
print("Difference between both results = ",(resultFromLoop-resultFromFormula)/resultFromFormula, "\n" )
Now that you have warmed up, you will need to compute the Fibonacci sequence (https://en.wikipedia.org/wiki/Fibonacci_number)
\begin{equation} 1,1,2,3,5,8,13,21,... \end{equation}Now the function below computes the $N$-th term of the Fibonacci sequence
In [ ]:
function fibonacciLoop(n)
fibn, fibn_p1 = (0,1) # nth and n+1th fibonacci numbers
for i = 1:n # defining a loop from 1 to n, with a default spacing of 1
fibn,fibn_p1 = (fibn_p1, fibn+fibn_p1) # computing the nex number of the series
end
return fibn
end
You can see that in order to reduce space and make the code more readable we useda block declaration within the function and within the loop
In [ ]:
a,b = (1,2)
print(a, "\n")
print(b, "\n")
You can see that in the example above, we declared and assigned a = 1 and b = 2, which is equivalent to
In [ ]:
a = 1
b = 2
print(a, "\n")
print(b, "\n")
We can use the same syntax to perform different operations such as:
In [ ]:
a,b = (b,a+b)
print(a, "\n")
print(b, "\n")
This piece of code assigns the value value of b to a, and the value of a plus b to b. In other words, we are updating the values of a and b.
Now you can check the validity of the fibonacciLoop function by runnin
In [ ]:
fibonacciLoop(8)
We can define a recursive function (a function that calls itself)
In [ ]:
fibonacciRec(n) = n < 2 ? n : fibonacciRec(n-1) + fibonacciRec(n-2)
You can check that both functions provide the same answer
In [ ]:
fibonacciRec(8)
For different implementation of the Fibonacci sequence please see http://www.scriptol.com/programming/fibonacci.php.
Morover, as you know, the Fibonacci sequence is the solution of a finite difference equation given by:
\begin{equation} f_{n+1} = f_{n} + f_{n-1} \end{equation}with boundary conditions $f_1 = 1$, $f_2 = 1$.
Show that the answer for this equation can be found in closed form. The explicit formula that is given by \begin{equation} f_n = \frac{1}{\sqrt{5}} \left [ \left ( \frac{1+\sqrt{5}}{2} \right )^n - \left ( \frac{1-\sqrt{5}}{2} \right )^n \right]. \end{equation}
In [ ]:
function fibonacciFormula(n)
return 1/sqrt(5)*(((1+sqrt(5))/2)^n - ((1-sqrt(5))/2)^n)
end
This gives a closed formula; however, the formula implies the multiplication by irrational numbers. These irrational numbers can not the excatly expressed using floating point numbers, so we are introducing some errors, and then multiplying those errors together. You can check the errors introduced by running
In [ ]:
print("Absolute error = ", abs(fibonacciFormula(40) - fibonacciRec(40)),"\n")
print("Relative error = ", abs(fibonacciFormula(40) - fibonacciRec(40))/fibonacciRec(40),"\n")
They provide the same result up to machine precission; however, we can check that the formula approach is much faster, to test how fast they are we will use the macro @time
In [ ]:
@time fibonacciFormula(8)
@time fibonacciRec(8)
As you can see, the two functions are almost as fast, (in fact fibonacciFormula lags a bit behind); however, if we try a bigger number
In [ ]:
@time fibonacciFormula(40)
@time fibonacciRec(40)
Now you can observe that fibonacciFormula is 5 order of magnitude faster that the recursive Function.
Now we can try to time the iterative function
In [ ]:
@time fibonacciLoop(200)
You can see that the iterative function is extremely fast but it provides a wrong answer. How is that possible?
Answer : The Fibonacci series grows fast, then after a while it gets out of range for int64.
We will modify fibonacciLoop2 slighlty to provide a better answer.
In [ ]:
function fibonacciLoop2(n)
fibn, fibn_p1 = (0.0,1.0) # nth and n+1th fibonacci numbers
for i = 1:n # defining a loop from 1 to n, with a default spacing of 1
fibn,fibn_p1 = (fibn_p1, fibn+fibn_p1)
end
return fibn
end
Can you explain the difference? In the first algorithm, all the arithmetic is performed using Int64 numbers. 0 is an Int64 and everything is added is an Int64. In the second version, by defining fibn, fibn_p1 = (0.0,1.0) we are forcing the algorithm to use Float64, that have a much greater range.
Now we can test the difference between Fibonacci using a loop and the formula.
In [ ]:
print("Absolute error = ", (fibonacciLoop2(300) - fibonacciFormula(300)), "\n")
print("Relative error = ", (fibonacciLoop2(300) - fibonacciFormula(300))/fibonacciFormula(300))
this means that both algorithms provide the same answer up to machine precission.
In [ ]:
In [ ]:
function factorialLoop(n)
nFac = 1
for ii = 1:n
nFac *= ii # this is equal to nFac = nFac * ii
end
return nFac
end
In [ ]:
factorialRec(n) = n < 2 ? 1 : n*factorialRec(n-1)
Test your functions using the Julia function factorial
In [ ]:
n = 17
print("error for iterative factorial = ", abs(factorial(n) - factorialLoop(n)), "\n")
print("error for recursive factorial = ", abs(factorial(n) - factorialRec(n)), "\n")
We will write a function, such that if for a number $m$ you will compute the largest integer $n$ such that $n! \leq m$
In [ ]:
function findLargerInteger(m)
largest = 1;
while (true) # we iterate until we find it
if factorial(largest+1) > m
break # if the next factorial is bigger than m, then we stop
else # it the next on is smaller than m, we continue searching
largest = largest+1; # we test if the next one is the biggest
end
end
return largest
end
We can test the function by running
In [ ]:
m = factorial(10)
findLargerInteger(m)
In [ ]:
function integrateRectangle(f, interval, Npoints)
h = (interval[2]-interval[1])/Npoints # compute the space step
Xquad = interval[1] + [1/2:1:Npoints-1/2]*h # compute the quadrature points
fquad = f(Xquad) # evaluate the function at the quadrature points
return sum(fquad)*h # sum everything and multiply by the weights
end
As you can see, a function can receive a function as an argument. Now you can use this function to compute integral numerically using the Rectangle Method https://en.wikipedia.org/wiki/Rectangle_method.