Big O is often used to describe how fast an algorithm is. However, it’s not quite right. It doesn’t measure the speed of the algorithm. Rather, it categorizes algorithms based on how radical their growth is. Formally, we say that it describes the complexity of the algorithm. This can be quite confusing for most people because the word “complex” is usually used to describe things that are difficult to understand. Additionally, the term complexity described by Big O is not for humans but rather for computers. It is even more confusing because it’s unclear what it means for a problem to be complex from a computer’s point of view.
Formally, we can write , which is read as “ is big O of ”, if there exists a positive real number such that:
where is the domain of . To put it simply, we say is big O of if is bounded by . If we can find a function and constant such that is always greater than , we can say that is bounded by and is big O of . Well, if we want to be very pedantic, it’s not really true, but it helps you to understand what big O is.
Here are some examples:
- , well because .
- , because , so by choosing , we can bound the function with .
- , because . So, by choosing , we can bound the function .
- , because always bounds .
- , because the function always outputs real numbers between zero and one, so .
- , because this function always outputs real numbers between zero and ten. By choosing , we can have .
- .
- , that is, the base of logarithmic complexity is not important since it is just a constant multiplier.
However, you can’t say that because no matter what you choose, there is always an such that .
Now, what is this all about? Well, technically, it can be anything. But usually, we can think of as some metric in our program that somehow represents the amount of work needed to be done by our algorithm. The number of “instructions” can be one of them. We can also think of it as the number of lines executed. It depends on what kind of thing we want to measure. In some cases, we don’t really care about CPU, and use the big O for the number of I/O operations. It’s up to us. Then, is the input of whatever you choose. Usually, it’s the size of our input. For example, in the context of sorting, we can define as the number of instructions needed to sort an array of integers.
Imagine you have two sorting algorithms, algorithms A and B. Algorithm A needs instructions to sort integers, and algorithm B needs instructions to sort integers. You may notice that both of them are , even though the first algorithm clearly needs 500 times more instructions. The big O notation doesn’t really track the constant. The constant doesn’t matter. What matters is only the growth. Both algorithms grow roughly the same way. If you make the input twice as large, they roughly need twice as many instructions.
People use Big O with some assumptions. In the context of an integer sorting algorithm, we usually assume that comparing two integers has a constant amount of work. On a modern CPU, comparing two 64-bit integers is done using a single CPU instruction, and checking the result is one more CPU instruction. This means we need roughly two CPU instructions for comparing integers and using the result. However, imagine if your integers are very big, like bytes in size. Comparing two -byte integers now may take roughly instructions. In this case, an algorithm that assumes comparison is , is actually if we consider the comparison to be .
Another example is calculating the -th term of the Fibonacci sequence using matrix exponentiation. People claim that we can calculate the -th term of the Fibonacci sequence using matrix exponentiation with complexity. However, this has one big assumption, i.e., multiplication is , which is true for most competitive programming problems because they usually want the answer to be taken modulo , forcing it to fit on a 32-bit integer, which takes roughly one CPU instruction to do the multiplication. In reality, the Fibonacci sequence grows quite fast. The 100th term of the Fibonacci sequence has more than 20 digits. Multiplying big integers shouldn’t be considered . In fact, there are integer multiplications with various complexities like Karatsuba with complexity, and fast Fourier transform with complexity.
So, the complexity of the algorithm depends on what our assumptions are and what the variables we care about are. Technically, we can say that every problem on competitive programming sites like Codeforces and LeetCode is , because their input is bounded by a constant constraint. Take Median of Two Sorted Arrays problem, for example. If you care about the constraints ( and ), you could find an efficient algorithm with complexity. However, since and are technically bounded by , you can say that your algorithm is . Whatever input you give to your algorithm, since the constraint is , your algorithm will stop in some maximum steps. It is just not very useful for the reader.