What are BigO Notation and Time Complexity?6 min read
When you try to solve a problem on some online websites, you probably see some terms like BigO notation, O(n), O(1), O(n²), etc… or time complexity couple of times, especially when solving an algorithm. So what they mean and what they are used for? If you don’t have an exact idea about these terms and want to figure out what they are and want to nostalgic back to your math skills in high school or college, this article is for you.
Time Complexity and Space Complexity
When trying to solve a problem, from writing flowchart to writing actual code, sometimes we can solve this problem by more than one way, it usually perhaps has several ways to solve. Hence, understanding the time complexity and also space complexity to compare the performance of different algorithms and choose the best one for a particular problem is crucial.
In computer science, the time complexity is the computational complexity that measures or estimates the time taken for running an algorithm.
Time complexity is commonly estimated by counting the number of elementary operations performed by the algorithm, supposing that an elementary operation takes a fixed amount of time to perform. In short, the time complexity of an algorithm quantifies the amount of time taken by an algorithm to run as a function of the length of the input. In the same way, space complexity is much like time complexity, we can define space complexity as the amount of space or memory taken by an algorithm run as a function of the length of the input.
In real cases, time and space complexity depends on many factors such as hardware, processor, memory, etc…But we don’t consider these cases for evaluating the algorithms, we just only consider the execution time for the algorithm.
Big O notation
When measuring the efficiency of an algorithm as the input size become larger, we usually express the rate of growth by some terms such as Big-θ (pronounced Big-theta) or Big- Ω (Big-omega). Simply speaking, Big-θ it has asymptotically tight bound whose the growth of the running time is ‘sandwiched’ between the lower bound and the upper bound, for the Big- Ω the growth of running time is just lower bound. Sometimes we cannot precisely present all the growth of running time by those two notations. Then we come with the Big O (Big-Oh) notation and wisely used because it covers all the cases of the growth of running time of a function.
Big O (Big-Oh) notation in computer science is used to describe the worst-case scenario in a particular algorithm in term of time complexity and/or space complexity such as execution time or the space used. Or you can say the maximum amount of time taken on inputs of a given size, which is Big O notation.
But strictly speaking, in computer science, big O notation is used to classify algorithms according to how their running time or space requirements grow as the input size grows. Big O notation characterizes functions according to their growth rates: different functions with the same growth rate may be represented using the same O notation.
The letter O is used because the growth rate of a function is also referred to as the order of the function. A description of a function in terms of big O notation usually only provides an upper bound on the growth rate of the function. Associated with big O notation are several related notations, using the symbols o, Ω, ω, and Θ, to describe other kinds of bounds on asymptotic growth rates as mentioned above.
It’s more clear with the example cases below:
O(1) Constant time: Given an input of size n, it only takes a single step for the algorithm to accomplish the task regardless of the number of items. Consider the example below:
In this case, O(n²) also called Quadratic time is used to describe an algorithm whose performance is directly proportional to the square of the size of the input data set. This is common with algorithms that involve nested loops. Deeper iterations will lead to O(n³), O(n&sup4;), O(n&sup5;) and so on…: A common sorting algorithm like the bubble sort, selection sort, and insertion sort runs in O(n²). Take a look at the example below:
O(2²) and O(n!)
O(2²) – Exponential time denotes an algorithm whose growths as double each input of the data set, this is common in situations when you traverse all the nodes in a binary tree. It usually exists in a recursive function, calculating Fibonacci number is one of those:
You can contemplate this code by opening up the debug mode in Chrome:
O(n!) – factorial time is common in generating permutation:
This case is a little bit hard to explain, but in a whole, O(log N) – Quasilinear time: in many cases, the n log n running time is simply the result of performing an O(log n) operation n times. It works by selecting the middle element of the data set, essentially the median, and compares it against a target value. If the values match it will return success.
The common technique using O(log N) is the binary search or looking up a number in a phone book when you keep breaking the binary tree or phone book (which are already sorted) into 2 smaller equal sub-arrays. Begin with the interval of the whole array (binary tree, phone book) if the value of the middle equals to the value you want to find, oh yeah, just return this value and the time complexity is the best scenario with a constant time O(1), however if the value is less than the middle then you narrow the interval in the left half, otherwise if the value is larger than the midst, then narrow it to the right half. Keep repeating this process until finding this value or the interval becomes empty.
How do I define when it is O(n), O(1), O(n²)?
Look at these examples above and also read the text carefully, you will get the idea. There are also some rules that you can apply to remember easier.
- General Rule #1: Return statements, initialize a variable, increment, assigning, etc. All of these operations take O(1) time.
- General Rule #2: The running time of the loop is at most the running time of the statements inside the loop multiplied by the number of iterations.
- General Rule #3: The total running time of the nested loops is the running time of the outer loop multiplied by the inner loop(s).