If we're not careful, making multiple recursive calls can lead to exponential running times real fast. Luckily, with a little thought, we can often avoid programs that unnecessarily repeat tremendous amounts of work.
Consider this natural implementation of the fib
function:
fun fib (n: int) =
if n < 1 then
0
else if n = 1 then
1
else
fib (n - 1) + fib (n - 2)
How many times will fib n
recurse? Let's call this number F(n)
.
Well, if n < 2
, then fib
doesn't recurse, so F(n) = 0
.
What about n >= 2
? In this case, fib n
makes two recursive calls, fib (n - 1)
and fib (n - 2)
, which make F(n - 2)
and F(n - 1)
recursive calls respectively. So in total, F(n) = 2 + F(n - 1) + F(n - 2)
.
n |
F(n) |
graph |
---|---|---|
0 | 0 | |
1 | 0 | |
2 | 2 | ** |
3 | 4 | **** |
4 | 8 | ******** |
5 | 14 | ************** |
6 | 24 | ************************ |
7 | 40 | **************************************** |
8 | 66 | ****************************************************************** |
9 | 108 | ************************************************************************************************************ |
That grows pretty fast... it looks exponential in fact. Basically, it's a non-starter.
But we can do better!
Consider this alternate implementation of fib
:
fun fib' (n: int) =
let
fun loop (i: int, f0: int, f1: int) =
if i = n then
f0
else
loop (i+1, f0 + f1, f0)
in
if n < 1 then
0
else
loop (1, 1, 0)
end
Using the loop
helper function, fib' n
only makes n
recursive calls. Yay!