Big-O Notation from a Non-CS Perspective
Tags: algorithms, beginners, javascript
Published: 2021-06-01 21:29:56

Hi everyone!

![Bear Says Hello](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iygn668v044tjh4mp7fh.gif)

Welcome to the second post in our Data Structures & Algorithm series! Last time we reviewed the crossovers in <a href="https://dev.to/mehmehmehlol/from-string-to-array-to-string-f67">JavaScript arrays and strings</a>. This time we will cover Big-O notation, diving into time and space complexity. 

Since both of us (<a href="https://dev.to/wlcreate">Waverley</a> and I) graduated from bootcamp, after learning Ruby on Rails, JavaScript, React, etc., we had to spend a lot of our time learning Big-O Notation through many online resources. We hope this will be the place for you if you are looking for a “plain English” explanation of Big-O Notation!

## Introduction
In computer science, Big-O notation is used to classify the run time or space requirements of an algorithm as their input size grows. For CS students at college, they have to learn different types of Big- notation (Big O, Big Theta, Big Omega). 

But for the sake of software engineering technical interviews, all we care about is the best and worst case scenarios. Although Big O describes an upper bound on the time in the CS concept, the industry uses Big O to try to offer the tightest description of the runtime. (Cracking the Coding Interview by Gayle McDowell provides a really great summary in these concept -- Read P.39) 

![Big O Notation Graph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3sahbrfd6besc18arjsr.jpeg)
This graph demonstrates clearly on how the run time and space changes depending on the input of a Big-O Notation. `O(1)` and `O(log n)` have the best run time and space complexity while `O(n!)`, <code>O(n<sup>2</sup>)</code> and <code>O(2<sup>n</sup>)</code> have the worst run time and space complexity.

In this article, we will break down all these notations with provided examples and Leetcode questions at the end of each part. 

## What does it mean by brute force and optimized solution?
Before we start, we would like to explain what brute force and optimized solution mean, as you might see these keywords later in the article. 

The easiest way to understand what **brute force solution** is whatever solution comes to your head first. On the other hand, for **optimized solution**, after you have the brute force solution, you would think of an optimized solution to either simplify the code or minimize the time and space complexity if possible. 

For instance, your brute force solution has a <code>O(n<sup>2</sup>)</code> time complexity and with optimized solution, you are able to reduce it to the time complexity of `O(n)`. 
Understanding this concept is important since this is something you would discuss with your interviewer on how you would make your solution from brute force to more optimized.

## Complexity Comparison

<table>
<tr>
<th>Name</th>
<th>Big O Notations</th>
</tr>
<tr>
<td>Constant Time</td>
<td>O(1)</td>
</tr>
<tr>
<td>Logarithmic Time</td>
<td>O(log n)</td>
</tr>
<tr>
<td>Linear Time</td>
<td>O(n)</td>
</tr>
<tr>
<td>Linearithmic Time</td>
<td>O(n log n)</td>
</tr>
<tr>
<td>Quadratic Time</td>
<td>O(n<sup>2</sup>)</td>
</tr>
<tr>
<td>Exponential Time</td>
<td>O(2<sup>n</sup>)</td>
</tr>
<tr>
<td>Factorial Time</td>
<td>O(n!)</td>
</tr>
</table>
---
### Constant Time: O(1)
Often referred to as “constant time”, `O(1)` has the least complexity. I like to think of this as no matter how large or small the input, you can always expect the same number of steps to execute inside the function.

Example:
```javascript
function sayHelloToFirstFriend(friends) {
   return `Hello ${friend[0]}`
}

sayHelloToFirstFriend([“spongebob”, “patrick”, “sandy”, “squidward”, “gary”])
// “Hello spongebob”
```

<table>
<th>Typical use cases</th>
<tr><td><a href=”https://dev.to/lukocastillo/time-complexity-big-0-for-javascript-array-methods-and-examples-mlg”>Accessing an Array through its Index</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/implementation-stack-javascript/”>Inserting (push) or deleting (pop) from a Stack</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/implementation-linkedlist-javascript/”>Inserting or deleting a node in a Linked List</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/implementation-queue-javascript/”>Insertion or deleting from a Queue</a></td></tr>
<tr><td><a href=”https://www.freecodecamp.org/news/how-to-implement-a-simple-hash-table-in-javascript-cb3b9c1f2997/”>Searching, inserting, or deleting from a Hash Table</a></td></tr>
</table>

### Logarithmic Time: O(log n)
Don’t be afraid of math! When you see a logarithm it’s asking you, “What power must we raise this base to, in order to get this answer?" In other words, we use logarithms to solve for a variable when that variable is an exponent. 

In terms of computer science this translates to: “How many times must we divide n in half in order to get back down to 1?” Therefore, solutions with `O(log n)` essentially divide the problem in half, determine which half it needs to continue, divide that section in half, repeating this same idea until it finds what it needs or ruling out the set. As a result, while these solutions do grow more than constant time, it nonetheless grows slowly compared to other time complexities.

<table>
<tr><th>Typical use cases</th></tr>
<tr><td><a href=”https://www.khanacademy.org/computing/computer-science/algorithms/binary-search/a/binary-search”>Binary Search</a></td></tr>
<tr><td><a href=”https://www.khanacademy.org/computing/computer-science/algorithms/merge-sort/a/linear-time-merging”>Certain Divide and Conquer Algorithms based on Linear functionality</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/count-fibonacci-numbers-given-range-log-time/”>Calculating Fibonacci Numbers</a></td></tr>
</table>

Note: Notice that for all of these use cases the input is sorted and searching for something!

### Linear Time: O(n)
Probably the most familiar is `O(n)`, or “linear time”. This is because as the size of the input grows, the time the number of operations takes to execute also grows. In other words, if an array has 10 items a for loop will be executed 10 times whereas if the array has 10,000 items the same for loop would execute 10,000 times as well.

Example 1:
```javascript
const binarySearch = (list, target) => {
  let start = 0
  let end = list.length - 1

  while (start <= end) {
    const middle = Math.floor((start + end) / 2)
    const guess = list[middle]

    if (guess === target) {
      return middle
    }

    if (guess > item) {
      // search the right side of the list
      end = middle - 1
    } else {
      // search the left side of the list
      start = middle + 1
    }
  }
  return null // if target is not found
}
```

Example 2:
```javascript
function sayHelloToFriends(friends) {
   for (let i = 0; i < friends.length; i++) {
      console.log(`Hello ${friends[i]}`)
   }
}

sayHelloToFriends([“spongebob”, “patrick”, “sandy”, “squidward”, “gary”])
// “Hello spongebob”
// “Hello patrick”
// “Hello sandy”
// “Hello squidward”
// “Hello gary”
```

<table>
<tr><th>Typical use cases</th></tr>
<tr><td>Traversing an <a href=”https://www.geeksforgeeks.org/ways-iterating-array-javascript/”>Array</a> or <a href=”https://www.geeksforgeeks.org/implementation-linkedlist-javascript/”>Linked List</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/linear-search/”>Linear Search</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/implementation-linkedlist-javascript/”>Deletion of a specific element in a Linked List (Not sorted)</a></td></tr>
<tr><td><a href=”https://www.freecodecamp.org/news/two-ways-to-check-for-palindromes-in-javascript-64fea8191fd7/”>Comparing two strings</a></td></tr>
<tr><td><a href=”https://www.freecodecamp.org/news/two-ways-to-check-for-palindromes-in-javascript-64fea8191fd7/”>Checking for Palindrome</a></td></tr>
<tr><td><a href=”https://www.freecodecamp.org/news/javascript-loops-explained-for-loop-for/”>Anytime using a `for` loop or iterating</a></td></tr>
</table>

### Linearithmic Time: O(n log n)
Building off of typical solutions for `O(log n)`, the extra “n” comes from the extra time cost of sorting. Therefore, a lot of sorting algorithms have the complexity of `O(n log n)`. On the other hand, while it does take more time than `O(log n)`, it’s also important to remember that logarithms grow very slowly. As a result, its path is similar to that of linear time. To explain a bit more of the role `n` plays, let’s take a look at merge sort. 

Starting the same as `O(log n)`, in merge sort you start by dividing the array in half. Next you sort the two halves and then merge the two sorted halves into one sorted whole. However, in order to sort the two halves you repeat the same idea of dividing them, sorting them, merging the sorted halves until you’ve sorted everything. 

Example: 
```javascript
function merge(left, right) {
    let arr = []
    // Break out of loop if any one of the array gets empty
    while (left.length && right.length) {
        // Pick the smaller among the smallest element of left and right sub arrays 
        if (left[0] < right[0]) {
            arr.push(left.shift())  
        } else {
            arr.push(right.shift()) 
        }
    }
    
    // Concatenating the leftover elements
    // (in case we didn't go through the entire left or right array)
    return [ ...arr, ...left, ...right ]
}

function mergeSort(array) {
  const half = array.length / 2
  
  // Base case or terminating case
  if(array.length < 2){
    return array 
  }
  
  const left = array.splice(0, half)
  return merge(mergeSort(left),mergeSort(array))
}
```
<table>
<tr><th>Typical use cases</th></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/merge-sort//">Merge Sort</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/heap-sort/”>Heap Sort</a></td></tr>
<tr><td><a href=”https://www.geeksforgeeks.org/quick-sort/”>Quick Sort</a></td></tr>
<tr><td><a href=”https://www.khanacademy.org/computing/computer-science/algorithms/merge-sort/a/divide-and-conquer-algorithms”>Certain Divide and Conquer Algorithms based on optimizing O(n<sup>2</sup>) algorithms</a></td></tr>
</table>

### Quadratic Time: O(n<sup>2</sup>)
A function with quadratic time complexity has a growth rate of n<sup>2</sup>. Meaning? If the input size is 2, then the function will take 4 operations. If the input size is 3, then the function will take 9 operations. If the input size is 1000, then the function will take 1,000,000 (1 million) operations.

In other words, <code>O(n<sup>2</sup>)</code> is going to run really slow, especially since the input size is really large. 

Most of the time, we would describe an algorithm that has quadratic time when we have to iterate within the object at least twice, like nested for loops.

Find duplicates and bubble sort are two of the quadratic algorithms examples that you would run into. Bubble sort (as well as insertion sort and selection sort) is like the naive version of merge sort and quick sort. It is slow, but it is always the first concept you would first learn when learning sorting algorithms. It builds a great foundation for the rest of the more complicated sorting algorithms.

What bubble sort does is repeatedly swapping adjacent elements if they are in the wrong order. Let's say we are sorting an unordered array of numbers from smallest to largest. Bubble sort would examine the numbers if they are in the right order by swapping them **one by one** .

Example of Bubble Sort:
```javascript
function bubbleSort(arr, n) {
  // double-loop of size n, so n^2
  for (let i = 0; i < n - 1; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        swap (arr, j, j+1);
      }
    }
  }
}

// swap helper method
function swap (arr, first, second) {
  let temp = arr[first];
  arr[first] = arr[second];
  arr[second] = temp;
}
```
With the nested loop, we have a time complexity of <code>O(n<sup>2</sup>)</code>

Comparing to Merge Sort, which the array would be cut in half , Bubble Sort would go through each element of the array **one by one** until everything is sorted in the right place (and then it will go through again one more time even though it is already sorted.)

<table>
<tr><th>Typical use cases</th></tr>
<tr><td><a href="https://www.geeksforgeeks.org/bubble-sort/">Bubble Sort</a></td></tr>
<tr><td><a href="https://www.geeksforgeeks.org/insertion-sort/">Insertion Sort</a></td></tr>
<tr><td><a href="https://www.geeksforgeeks.org/insertion-sort/">Selection Sort</a></td></tr>
<tr><td><a href="https://leetcode.com/problems/find-the-duplicate-number/">Find Duplicates (Brute Force)</a></td></tr>
<tr><td><a href="https://www.geeksforgeeks.org/find-all-pairs-possible-from-the-given-array/">Find All Possible Ordered Pairs in An Array</a></td></tr>
</table>

### Exponential Time: O(2<sup>n</sup>)
Base-2 Exponential running time means the calculations will go double with each input size grows.
2<sup>2</sup> => 4
2<sup>3</sup> => 8
2<sup>4</sup> => 16
...
2<sup>100</sup> => 1,267,650,600,228,229,401,496,703,205,376

As you can see whenever `n` is increased by 1, the result is doubled. Essentially, the number starts very low and to the end, the number will be very large.

In most cases, please avoid the use of exponential time since the run time is going to get slower. Not that it's the worst one, but obviously it's not great. 

Fibonacci Example
```javascript
function fib(n) {
  if (n <= 1) {
    return n
  }
  return fib(n - 1) + fib (n - 2)
}
```

<table>
<tr><th>Typical use cases</th></tr>
<tr><td><a href="https://www.educative.io/m/find-all-subsets">Power Set: Finding all the subsets on a set</a></td></tr>
<tr><td><a href="https://leetcode.com/problems/fibonacci-number/">Fibonacci Number</a></td></tr>
</table>

### Factorial Time: O(n!)
If you understood how factorial works, this is how it's like:
5! = 5 x 4 x 3 x 2 x 1, in other words,
n! = n x (n - 1) x (n - 2) x (n - 3)... x 1

As the input size increases, the run time gets bigger and bigger and BIGGER! I personally have not encountered a factorial problem, therefore I would attach an example below with the link as reference.

<table>
<tr><th>Typical use cases</th></tr>
<tr><td><a href="https://adrianmejia.com/most-popular-algorithms-time-complexity-every-programmer-should-know-free-online-tutorial-course/#Permutations">Permutations</a></td></tr>
</table>

---
## Conclusion
We hope this article gives you a better understanding of Big-O Notation! This concept is important since often during interviews, you will need to analyze the Big-O Notation of your solution. Furthermore, knowing this can help you understand which solution has better or worse runtime as you come up with approaches. If you are still having trouble understanding, we have provided more resources down below for you to reference! 

---
## Resources
<ol>
<li><a href="https://stackoverflow.com/questions/1592649/examples-of-algorithms-which-has-o1-on-log-n-and-olog-n-complexities">Examples of Algorithms which has O(1), O(n log n) and O(log n) complexities 👀</a> (Stack Overflow)</li>
<li><a href="https://www.bigocheatsheet.com">Big-O Cheat Sheet</a></li>
<li><a href="https://www.freecodecamp.org/news/big-o-notation-why-it-matters-and-why-it-doesnt-1674cfa8a23c/">What is Big O Notation Explained: Space and Time Complexity</a> (FreeCodeCamp)</li> 
<li><a href="https://en.wikipedia.org/wiki/Big_O_notation">Big-O notation</a> (Wikipedia)</li>
<li><a href="https://adrianmejia.com/most-popular-algorithms-time-complexity-every-programmer-should-know-free-online-tutorial-course/">8 time complexities that every programmer should know</a> (With Videos and Examples)</li>
<li><a href="https://web.stanford.edu/class/cs9/sample_probs/TwoSum.pdf">Comparing different solutions for Two Sum</a> (Stanford)</li>
</ol> 
</li>
</ol> 

</ol>