The most optimal solution to FizzBuzz, yes really

April 2nd 2023 | ~ 4 minute read

Note: It has been brought to my attention that my solution isn't "optimal", because it's not the most performant. That's not what I mean by optimal. My solution optimizes for readability and extensibility, not performance.

Introduction

FizzBuzz is an interesting problem to solve programmatically. It seems so simple until you actually try to solve it yourself. The problem goes like this:

Print out the numbers 1 - 100, whereby replacing every number divisible by 3 with Fizz, every number divisible by 5 with Buzz and with FizzBuzz if they're divisible by both.

It's interesting due to the fact that it's rather difficult for beginner programmers to solve optimally. It has thus been extensively used as an interview question for aspiring software engineers. And while it has fallen out of favor these days, I still think it can still give you insight into how someone approaches writing solutions to problems. Do they hastily implement a naive solution, or do they think more deeply about the possibility of changing requirements?

Solutions

Let's take a look at some solutions you may find in the wild. I'll be writing in Go for my convenience, but you may use any general purpose programming language to solve it.

A naive approach

Most beginners will code a solution that looks something like this:

package main

import (
    "fmt"
)

func main() {
    for i := 1; i <= 100; i++ {
        if i%3 == 0 && i%5 == 0 {
            fmt.Println("FizzBuzz")
        } else if i%5 == 0 {
            fmt.Println("Buzz")
        } else if i%3 == 0 {
            fmt.Println("Fizz")
        } else {
            fmt.Println(i)
        }
    }
}

While this solution works, it's not pretty nor is it easy to extend. What if we now wanted it to work for 7s or 13s? We would end up adding more if statements and further cluttering the code.

A slightly better solution

Tom Scott actually made a video about FizzBuzz and his proposed solution goes like this:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    for i := 1; i <= 100; i++ {
        output := ""

        if i%3 == 0 {
            output += "Fizz"
        }

        if i%5 == 0 {
            output += "Buzz"
        }
        
        if i%7 == 0 {
            output += "Fuzz"
        }

        if output == "" {
            output = strconv.Itoa(i)
        }

        fmt.Println(output)
    }
}

This is better, especially since the code is less cluttered and more extensible. Notice that the code now works for numbers divisible by 7 as well. However, I propose a modification to Tom's code that I think to be the optimal solution to FizzBuzz (if you don't count the glorious enterprise edition of course).

My solution

My biggest gripe with the code is that there's still too many if statements for my liking, ideally there should be only one or two. So, let's fix that.

package main

import (
    "fmt"
    "strconv"
)

func main() {
    fizzMap := map[int]string{
        3:  "Fizz",
        5:  "Buzz",
        7:  "Fuzz",
        13: "Fooz",
        34: "Booz",
    }

    for i := 1; i <= 100; i++ {
        output := ""
        for k, v := range fizzMap {
            if i%k == 0 {
                output += v
            }
        }

        if output == "" {
            output = strconv.Itoa(i)
        }

        fmt.Println(output)
    }
}

With this solution we use a hash map to avoid testing every possible case separately. It's also very easy to extend as all we have to do to add additional cases is to modify the map.

The more pedantic of you will point out that my algorithm is slower than the original, due to nested for loops, but I will say that's a non issue within the constraints of the input size posed in the original problem. My solution will not run noticeably slower with the input size of 100.

This is also a nice opportunity to learn what the hell Big O notation actually means for performance.

Conclusion

While I would not recommend judging candidates solely on their ability to optimally solve FizzBuzz the kind of feedback you receive with the follow up questions is, in my opinion, a good indicator of someone's approach to problem solving. A good follow up question is something along the lines of: "Can you implement this solution while reducing the number of if statements?" If they answer no, that's a pretty good sign that they don't really think outside the box, but hey, that's just my opinion.