/ #Algorithms 

Algorithms Techniques - Part 3: Recursion

An overview of recursive algorithms.

A recursive algorithm is an algorithm that calls itself repeatedly until a certain condition is met. For problem solving, it means that we can apply the same algorithm multiple times to a smaller subset of the original input until we find a solution.

Imagine you have a glass full of M&M’s and you want to empty the glass, but you can only remove one M&M at the time. It would work like this:

Is the glass empty? No, remove 1 M&M.
Is the glass empty? No, remove 1 M&M.
Is the glass empty? No, remove 1 M&M.
… Is the glass empty? Yes, stop.

Using pseudo-code:

function empty_glass(int n):
    if n equals 0:
        // Do nothing
    else:
        empty_glass(n - 1)

This is a very simple example that could’ve been solved using a simple for loop, but recursion is still the best (or at least more elegant) approach for some classes of problems.

Fibonacci

A classic example of a problem that can be solved using recursion is calculating a Fibonacci number. Fibonacci numbers form a sequence starting with 1 and 1 where each number is the sum of the previous 2 numbers:
1, 1, 2, 3, 5, 8, …

And we have the following Fibonacci numbers:
The 1st number is 1
The 2nd number is 1
The 3rd number is 2
The 4th number is 3
The 5th number is 5
The 6th number is 8
… and so on

Being n the Fibonacci number you want, this can be expressed as:

F(n) = F(n - 1) + F(n - 2)

Let’s see the implementation in Go:

package main

import "fmt"

func Fb(n int) int {
    // If n is 0 or 1, return it
    if n < 2 {
        return n
    }

    return Fb(n-1) + Fb(n-2)
}

func fibonacci() {
    for n := 1; n <= 6; n++ {
        fib := Fb(n)
        fmt.Printf("Fibonnacci (%d): %d\n", n, fib)
    }
}

Running this will produce:

Fibonnacci (1): 1
Fibonnacci (2): 1
Fibonnacci (3): 2
Fibonnacci (4): 3
Fibonnacci (5): 5
Fibonnacci (6): 8

The good and the bad

Recursion has its pros and cons. For some problems, like traversing a tree, recursion provides an easier to understand and somewhat more elegant solution. However, it can be slower, especially if the recursion is too deep.

Running a quick benchmark on the code above will show us that the speed decreases the deeper the recursion goes:

package main

import "testing"

// This is the actual test
func benchmarkFb(i int, b *testing.B) {
    for n := 0; n < b.N; n++ {
        Fb(i)
    }
}

// The functions below are different benchmarks, each with a different input
// to our Fibonacci function.

func BenchmarkFb1(b *testing.B) {
    benchmarkFb(1, b)
}

func BenchmarkFb2(b *testing.B) {
    benchmarkFb(2, b)
}

func BenchmarkFb3(b *testing.B) {
    benchmarkFb(3, b)
}

func BenchmarkFb10(b *testing.B) {
    benchmarkFb(10, b)
}

func BenchmarkFb25(b *testing.B) {
    benchmarkFb(25, b)
}

func BenchmarkFb45(b *testing.B) {
    benchmarkFb(45, b)
}

We can see that in the output:

BenchmarkFb1-4                  434873206                2.55 ns/op
BenchmarkFb2-4                  174979096                6.76 ns/op
BenchmarkFb3-4                  100525826               11.8 ns/op
BenchmarkFb10-4                  2466976               457 ns/op
BenchmarkFb25-4                     1856            613773 ns/op
BenchmarkFb45-4                        1        9282143600 ns/op

Cover picture by Simon Hurry via Unsplash