Optimising recursive algorithms via memoisation

Naive implementations of some recursive algorithms might be expensive. For example, to calculate the Fibonacci numbers, the following code can be used:

fn fib1(n: Int) -> Int:
	if n <= 1:
		return n
	return fib1(n - 1) + fib1(n - 2)

Fibonacci numbers are usually defined such that f(0) = 0, f(1)=1, f(n) = f(n-1) + f(n-2). So in the code above there are two recursive calls. To try out the implementation let's implement a loop:

fn main():
	for n in range(45):
		print(n, fib1(n))

If you now run time mojo fib.mojo it turns out that the program takes several seconds to run (5s on my computer). This is because there are too many recursive calls with the same input.

We might now try to store the results of the function and simply take them from a dictionary on subsequent calls:

var fibmap = Dict[Int, Int]()

fn fib2(n: Int) -> Int:
	if n <= 1:
		return n
	var r = fibmap.get(n)
	if r:
		return r.value()[]
	else:
		var r = fib2(n - 1) + fib2(n - 2)
		fibmap[n] = r
		return r

The fibmap dictionary contains the Fibonacci numbers calculated so far so no recursive calls are needed to get f(n) if it's already been calculated. If we now run

fn main():
	for n in range(45):
		print(n, fib2(n))

the program finishes in less that a second.

The presented technique is usually called memoisation (or tabling) and is an algorithmic paradigm of dynamic programming.

AlgorithmsMojoPython
Avatar for Petr Homola

Written by Petr Homola

Studied physics & CS; PhD in NLP; interested in AI, HPC & PLT

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.