5 Steps to Crushing Big O
By: Alexandra Ortiz
Tags: Data Structures, Big O Notation, Ruby, Coding, Asymptotic Notation

This linear runtime is represented in the chart below in green. See how the time increases at a steady and predictable rate as the data input grows? That is what linear means, a straight line! If we think back to our previous O(1), or constant time, example, we see that the runtime stays, well, constant no matter how much our data input, n , grows.

Graph showing the various types of algebraic runtimes in Big O. This is not all of the possible runtimes, just the most common.

So when we think of steps, think of it in terms of what we are doing with the input and how that would change with size. We’ve covered what O(1) and O(n) would look like, but there are numerous other types of Big O runtimes.

Quadratic time, or O(n²), for example, is commonly found if you are making your way through 2 nested loops. If we look at the example below, we can see that the function finding_sum_of_nested_arrays is taking a two-dimensional array (an array of arrays), array_2d , and looping through each row and then each element to get the sum. Here, we can describe the outer loop as running n times and the inner loop as running n times for every outer loop iteration, or n².

Example of O(n²) runtime with nested loops

If you see a function with 3 nested loops, then it will likely be O(n³) or cubic time. Four nested loops would be O(n⁴) and so on.

When in doubt just add the number of nested loops and you should be on a good path.

Step #2: Different steps get added

Ok, so we’ve had a quick introduction into loops, but what about algorithms that have more than one thing going on? Great question! One important thing to remember about Big O is that n has to mean something. If you are dealing with more than one something, then you are going to need more than one variable. Also, n is just a common practice variable but we can easily describe all of the functions we’ve seen so far as something else, like O(4), O(a), O(b²), etc.

So let’s take a look at the following function.

Example of a function where the variables are added for Big O

Similar to our previous examples, we are dealing with two loops in one function. This function, however, does not have the loops nested. Instead, we loop through array_A , print the results, then move on to array_B and do the same thing. In this example, we need two variables to describe what is going on because we are taking the runtime for each input. Here, the two inputs are independent of each other and do not compound, so our time complexity would be O(a + b).

The temptation to multiply the two inputs, or O(a*b), is strong, but we must remember that Big O is about how the algorithm scales as the inputs increase. In the scenario above, array_A is not effected if array_B grows, so we must treat them as two distinct variables. Gayle Laakmann McDowell, author of “Cracking the Coding Interview”, put it best:

“If your algorithm is in the form ‘do this, then, when you’re all done, do that’ then you add the runtimes. If your algorithm is in the form ‘do this for each time you do that’ then you multiply the runtimes.”

Step#3: Drop constants and coefficients

Yup, you read that right! When it comes to Big O, we don’t care about constants or coefficients because what we are really after is how the function scales.

Take, for example, the code above which can be described as O( 2n +10), with 2n coming from two loops executing the single array and 10 from the last line that prints out hi ten times. Even though we might be tempted to say definitively that it is O( 2n +10) and nothing else, we would be incorrect. Again, we care about the scale of this algorithm so we need to drop the constants and the coefficients first.

What is a constant? In this case, it is 10 in O( 2n +10). This makes sense to drop because 10 will never change even if our input of array increases by 1000%.

So now we are left with O(2n). In this case, 2 is our coefficient meaning that it is a numerical value which we are multiplying our variable by. Again, we are interested in the scale of our algorithm so saying O(2n) vs O(10n) vs O(n) does not change the fact that it will always have a linear runtime.

At the end of the day, neither constants nor coefficients play a significant role as our data input, n , gets closer to infinity.

Step#4: Drop non-dominant terms

Ok, so this one seems a little tricky and counter intuitive but stay with me. Let’s say we have an algorithm we’ve calculated as O(n² + 4n³), how can we pare this down?

First, we want to go for the low-hanging fruit and get rid of the coefficient 4 for reasons previously discussed. Now we are left with O(n² + n³). Remember that Big O is all about scale, so we want to drop the non-dominant term, aka the part(s) of the expression that play a less significant role as our input increases. In this case, our non-dominant term would be n², leaving us with O(n³) as our runtime. Let’s put this in terms of numbers:

Pseudocode illustrating how dominant and non-dominant terms change as N gets larger

In the first example, we set n = 10 . When we square n (n² or n ** 2 ), the result is 100 . If we cube n (n³ or n ** 3 ), our result is 1000 , just 10x more than n². That doesn’t seem like a lot but see what happens in our second example when n = 1,000,000,000 . Now the difference between n² and n³ is much more significant and we can see that n³ will only continue to eclipse n² more and more as we approach infinity.

Step #5: Different inputs get different variables

Finally, when dealing with multiple inputs, we must remember to use multiple variables. After all, n in O(n) is a data input, not just a random letter someone decided to stick between parenthesis. If we have multiple data inputs then they each must have their own unique variable to acknowledge their relationship.