Jauhar's Blog


Back to Basics: Intro to Big O

2025/12/18

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 f(x)=O(g(x))f(x) = O(g(x)), which is read as “f(x)f(x) is big O of g(x)g(x)”, if there exists a positive real number MM such that:

∣f(x)∣≤Mg(x)for allx∈D |f(x)| \le M g(x) \quad \text{for all} \quad x \in D

where DD is the domain of ff. To put it simply, we say ff is big O of g(x)g(x) if ff is bounded by gg. If we can find a function gg and constant MM such that M×g(x)M\times g(x) is always greater than f(x)f(x), we can say that ff is bounded by gg and ff is big O of gg. 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:

However, you can’t say that x2=O(x)x^2 = O(x) because no matter what MM you choose, there is always an xx such that x2>M×xx^2 > M\times x.

Now, what is this ff all about? Well, technically, it can be anything. But usually, we can think of ff 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, xx is the input of whatever ff you choose. Usually, it’s the size of our input. For example, in the context of sorting, we can define f(x)f(x) as the number of instructions needed to sort an array of xx integers.

Imagine you have two sorting algorithms, algorithms A and B. Algorithm A needs 1000N21000N^2 instructions to sort NN integers, and algorithm B needs 2N22N^2 instructions to sort NN integers. You may notice that both of them are O(N2)O(N^2), 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 SS bytes in size. Comparing two SS-byte integers now may take roughly 2×S2\times S instructions. In this case, an O(N2)O(N^2) algorithm that assumes comparison is O(1)O(1), is actually O(N2×S)O(N^2\times S) if we consider the comparison to be O(S)O(S).

Another example is calculating the NN-th term of the Fibonacci sequence using matrix exponentiation. People claim that we can calculate the NN-th term of the Fibonacci sequence using matrix exponentiation with O(logN)O(log N) complexity. However, this has one big assumption, i.e., multiplication is O(1)O(1), which is true for most competitive programming problems because they usually want the answer to be taken modulo 109+710^9+7, 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 O(1)O(1). In fact, there are integer multiplications with various complexities like Karatsuba with O(Nlog23)O(N^{log_2 3}) complexity, and fast Fourier transform with O(NlogN)O(Nlog N) 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 O(1)O(1), because their input is bounded by a constant constraint. Take Median of Two Sorted Arrays problem, for example. If you care about the constraints (mm and nn), you could find an efficient algorithm with O(log(n+m))O(log(n+m)) complexity. However, since mm and nn are technically bounded by 10001000, you can say that your algorithm is O(1)O(1). Whatever input you give to your algorithm, since the constraint is 0≤m,n≤10000 \le m, n \le 1000, your algorithm will stop in some maximum steps. It is just not very useful for the reader.