Nowadays, you can definitely make it as a programmer without any theoretical computer science background, and there isn’t any problem with that, awesome things can be built from scratch without knowing stuff like Big . Yet there are many cases where having a grasp of how your algorithms grow can save you from a lot of trouble. Big notation (or Landau’s symbol) in computer science is used to classify algorithms based on how efficient they are or how much space they need as their input size grows.
Some may call the notation Landau’s symbol, because it was invented by German mathematician, Edmund Georg Hermann Landau (1877 – 1938), while others call it Big notation, because the letter refers to the rate of growth of a function, also called its order. Throughout this article, we’ll use the latter appellation. It basically describes how fast a function grows or declines, and is applied in several scientific fields, such as mathematics, complexity theory, and computer science. In this article, I’ll try to give you a detailed overview of this useful tool, going into how its applied in computer science, and describing it mathematically.
Your searching algorithm probably runs fast enough when it’s used on an array that has the size of 100, but what would happen if you ran it on an array that has the size of 10000? A more appropriate word instead of fast would be efficient. It is important to mention that when we measure an algorithm’s efficiency, we’re not talking about a real measurement of time. You cannot say that your code runs in seconds on every computer, since people’s hardware vary widely. Instead, Big measures the asymptotic complexity of a program. To illustrate this, I compared the runtime of linear search & binary search on sorted arrays that had the size of up to 1 million. An sized array contained the numbers from to , and the algorithms always searched for the value of in the array to demonstrate worst-case scenarios - that is, that the element we want is not included in the array. The exact results are different on every machine, because of hardware differences, but the way the runtime grows should be similar on every one of them. Let’s start with linear search.
Linear search (also known as sequential search) is a method for finding an element within a list or array.
As the name suggests, it sequentially checks each element of the array, until it founds a match or reaches the end of the array.
Implemented in Python:
def linearSearch(arr, x):
for i in range(len(arr)):
if arr[i] == x:
return i
return -1
Using the code given above, I got the following results:
As you can see in the diagram, the bigger the input size was, the longer it took to find the element we’re looking for. The runtime grew proportionally with respect to the input size. Below is some of the data I used to create this chart. In the first column, you see the size of the array, which the algorithm was executed on, and in the second, the time the execution required in milliseconds:
1000 0.276
2000 0.553
4000 1.076
8000 2.22
16000 4.317
For the algorithm to terminate, in an array that had the size of 2000, took roughly twice as long as it had taken to find an element in an array with the length of 1000 - we’re strictly talking about worst-case scenarios, of course. The algorithm had to look at all of the elements of an -sized array, one-by-one. As increased, the time required to complete the algorithm increased linearly. Thus, we say that linear search runs in
This is why you see the linear pattern on the chart. However, In real life scenarios, linear search is rarely used, because there are other, better alternatives, such as binary search, which offers significantly faster searching.
Binary search (also known as half-interval search, logarithmic search or binary chop) is a searching algorithm that finds the position of a target within a sorted array.
The initial search region is the entire array, meaning if the target exists, it must be between the first and last element of the array. To express this view programatically, we initialise two variables in the beginning, called lower bound & upper bound, and set the former to zero, and the latter to the length of the array minus one.
This process is then repeated until a match is found or the search terminates as unsuccessful. Here’s a visualisation of this procedure:
Binary search implemented in Python:
def binarySearch(arr, x):
lowerBound = 0
upperBound = 0
right = len(arr) - 1
while lowerBound <= upperBound:
mid = (lowerBound+upperBound) // 2
if x == arr[mid]:
return mid
elif x < arr[mid]:
upperBound = mid - 1
else:
lowerBound = mid + 1
return -1
Now let’s take a look at its runtime, just like we did with linear search:
You may have already noticed by looking at the y-axis that how much faster binary search is. It peaks a little bit above 0.1 ms — in comparison, linear search reached 250 ms with bigger sized arrays on my computer. Though, the most important difference is that the linear pattern is gone. This is replaced by a logarithmic one. That is because with each iteration, the algorithm halves the search region. You start with elements, then you get to , then and so on, until you get to a problem the size of 1. This process can be described the following way:
The question is, how many times does the algorithm have to divide by 2 to get to 1? If we solve for , we get . Thus, we say that binary search runs in
Below is a list of common functions programmers encounter when they analyse their algorithms, from slowest to fastest growing:
Big notation, by definition, means:
A mathematical notation that describes the limiting behaviour of a function when the argument tends towards a particular value or infinity
First of all, there are many notations that can be used to describe the asymptotic behaviours of functions. The three most important are big (the letter , not the number zero), big (big Omega) and big (big Theta), but there are also little o and little (little omega). The latter two are mostly only used in mathematics. Essentially, with these notations, you can get a sense of how a function grows over time.
With the help of big , we can say that an algorithm takes at most a certain amount of time or number of steps to execute. In the previous section we explored that binary search runs in — but it’s also correct to state that it runs in or , however, it is imprecise. It’s a best practice to always give the tightest bound there is — in the case of binary search, .
Mathematically, big notation gives an asymptotic upper bound. Formal definition:
Which reads as: is big of if there exist a positive constant , and positive integer , such that for all , the absolute value of f(n) is less than or equal to times the absolute value of g(n).
This basically means that does not grow faster than . It’s important to mention that in computer science usually only positive functions with natural numbers as arguments are concerned, hence absolute values can be ignored.
Example:
Let
By plugging in we get:
Now, let’s try to replace with different values, until the inequality above becomes true:
As it can be seen in the table, for all 5, the inequality becomes true. Therefore, we can say that:
This can also be visualised if we graph the two function:
To calculate the big of any function:
Example:
Suppose:
By removing the constants, we get:
Then, removing the coefficients:
And finally, the more dominant term between and is , therefore:
There are other symbols we can use to describe the asymptotic behaviour of functions. One of them is big (Omega).
As opposed to Big , with Big , we can say that an algorithm takes at least a certain amount of time, or number of steps to execute. If you have an algorithm that runs in , then it’s also technically correct to state that it runs in - but it’s horribly imprecise. We must give the tightest bound possible.
Mathematically, big gives an asymptotic lower bound. Formal definition:
Which reads as: is big Omega of if there exist a positive constant , and positive integer , such that for all , the absolute value of is greater than or equal to times the absolute value of .
This basically means that does not grow slower than .
Example:
Let
By plugging in we get:
Which is always true for all positive . Therefore, we can say that:
Visualised:
To calculate the big of any function:
Example:
Suppose:
By removing the constants, we get:
Then, removing the coefficients:
And finally, the least dominant term is , therefore:
Now, as I mentioned in the introduction of this section, there is also a third notation commonly used in computer science - Big (theta).
If you want to emphasise how good or bad an algorithm really is, you should always describe its big (if possible), because that is the tightest achievable bound one can give. The reason for that is because the function which gives the amount of time required to execute your algorithm () is bounded both from above and from below by . In other words, its big and big is the same:
Formal definition:
Which reads as: is big of if and only if there exist positive constants , , and positive integer , such that for all , the absolute value of is greater than or equal to times the absolute value of and is smaller than or equal to times the absolute value of .
Example:
Previously, we saw that for , is and also . Thus, we can say that is - and as you can see is “sandwiched” between :
However, if you want to show how well an algorithm runs, and big is unachievable, then the next best thing to use is big .
It’s important to mention that using the equals sign is technically incorrect. As Nicolaas Govert de Bruijn, dutch mathematician said: is true but is not. Using the equals sign suggests transitivity.
Example.
If:
We should be able to conclude that:
But one is not allowed to do that as it is not the case for all greater than some other number. Instead, it’s more appropriate to use set notation, e.g.:
Though, in most cases, people use equals sign, simply out of convention.
In summary, Big notation is a tool that enables computer scientists and developers to analyze the efficiency and scalability of algorithms by classifying them into categories like constant, logarithmic, linear, and so on, though it should only be taken as a rough approximation as it ignores constant factors, and lower-order terms. It provides a standardized way to express the upper bound (with big ), the lower bound (with big ), and the tight bound (with big ) of a function. Taking these into account inevitably results in better performance and scalabality in software systems.