DEV Community

Cover image for Optimization exercise with Rust
Daniel Di Dio Balsamo
Daniel Di Dio Balsamo

Posted on

4

Optimization exercise with Rust

Introduction

Optimizing code is something we all need to do everyday as developers.
But trying to immediately find the best solution is slow, whereas starting from a naive solution that you incrementally improve is more efficient.

Let's see this through a dynamic programming problem, the Fibonacci sequence, and using Rust.
This problem will be solved with 4 different approaches:

  • naive solution
  • top-down approach (memoization)
  • bottom-up approach
  • optimized bottom-up approach

Naive solution

The Fibonacci sequence is defined as follows:

Fn={F0=0F1=1Fn=Fn1+Fn2for n>1F_n = \begin{cases} F_0 = 0\\ F_1 = 1\\ F_n = F_{n-1} + F_{n-2} &\text{for n>1} \end{cases}

Let's start with a naive, yet simple implementation :

fn fibonacci(n: usize) -> u128 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn main() {
    println!("{}", fibonacci(35));
}
Enter fullscreen mode Exit fullscreen mode

The corresponding time complexity is O(2n)O(2^n) : every fibonacci call triggers 2 calls, and we're doing that n times.

Exponential runtime... we must avoid that.

Top-down approach (memoization)

A lot of intermediate results are identical: fibonacci(5) computes 3 times fibonacci(2) for example.
Caching the results would avoid to compute them again and again, let's do that.

fn fibonacci(n: usize, memo: &mut [u128]) -> u128 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            if memo[n] == 0 {
                memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
            }
            memo[n]
        }
    }
}

fn main() {
    let n = 35;
    println!("{}", fibonacci(n, &mut vec![0u128; n + 1]));
}
Enter fullscreen mode Exit fullscreen mode

Now the time complexity is O(n)O(n) , since the cached cases immediately return.
But we can do better : this array becomes very big if we try to compute big Fibonacci numbers.
Before removing this array, we need to take another approach.

Bottom-up approach

Let's consider again the first naive solution. It is slow because of recursive calls.
Let's unstack them and store intermediate results :

fn fibonacci(n: usize) -> u128 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let mut memo = vec![0; n + 1];
            memo[0] = 0;
            memo[1] = 1;

            for i in 2..=n {
                memo[i] = memo[i - 1] + memo[i - 2];
            }

            memo[n]
        }
    }
}

fn main() {
    println!("{}", fibonacci(35));
}
Enter fullscreen mode Exit fullscreen mode

But wait, we're only considering memo[i - 1] and memo[i - 2], why do we store other results ?

Last optimization

We only need the two last results, let's use two simple variables for this :

fn fibonacci(n: usize) -> u128 {
    if n == 0 {
        return 0;
    }

    let (mut a, mut b) = (0, 1);

    for _ in 2..n {
        let c = a + b;
        a = b;
        b = c;
    }

    a + b
}

fn main() {
    println!("{}", fibonacci(35));
}
Enter fullscreen mode Exit fullscreen mode

Finally, the time complexity is still O(n)O(n) , but without the array as in the top-down solution.

Conclusion

Starting from a naive solution allows you to make sure you really understood the problem.
Then you can iterate until your code is optimized enough.

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay