| Start | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| A Simple Loop The following function finds the maximal element in an array: The input size is the size of the array, which I called Let's count the operations. These two assignments are done only once, so that's 2 operations. The operations that are looped are: Since there are 3 operations in the loop, and the loop is done n times, we add You probably have noticed that "operation" is not very well defined. For instance I said that |
||||||||||
| A Nested Loop The following function checks if an array has any duplicates by taking each element, then iterating over the whole array to see if the element is there The inner loop performs at each iteration a number of operations that is constant with As you may have noticed, we can improve the algorithm by avoiding doing the same comparisons multiple times. We can start from Obviously, this second version does less operations and so is more efficient. How does that translate to Big-O notation? Well, now the inner loop body is run |
||||||||||
An O(log n) example IntroductionConsider the following problem:
Naïve approachThe first thing that comes to mind is to just read every index until 0 is found. In the worst case, the number of operations is This works fine for small values of DichotomyConsider the following algorithm (Python3):
In the worst case, we have to wait until ExplanationNote: When we write "log", we mean the binary logarithm, or log base 2 (which we will write "log_2"). As O(log_2 n) = O(log n) (you can do the math) we will use "log" instead of "log_2". Let's call x the number of operations: we know that 1 = n / (2^x). So 2^x = n, then x = log n ConclusionWhen faced with successive divisions (be it by two or by any number), remember that the complexity is logarithmic. |
||||||||||
| O(log n) types of Algorithms Let's say we have a problem of size n. Now for each step of our algorithm(which we need write), our original problem becomes half of its previous size(n/2). So at each step, our problem becomes half.
When the problem space is reduced(i.e solved completely), it cannot be reduced any further(n becomes equal to 1) after exiting check condition. Now we know that our algorithm can run maximum up to log n, hence
time complexity comes as A very simple example in code to support above text is : So now if some one asks you if n is 256 how many steps that loop( or any other algorithm that cuts down it's problem size into half) will run you can very easily calculate. k = log2 256 k = log2 2 8 ( => logaa = 1) k = 8 Another very good example for similar case is Binary Search Algorithm. RemarksDefinition The Big-O notation is at its heart a mathematical notation, used to compare the rate of convergence of functions. Let Actually the scope of the Big-O notation is a bit wider in mathematics but for simplicity I have narrowed it to what is used in algorithm complexity analysis : functions defined on the naturals, that have non-zero values, and the case of n growing to infinity. What does it mean ? Let's take the case of Let's find out if This means that f tends to infinity at approximately the same speed as g. Now this may seem like a strange thing to say, because what we have found is that f is at most 111 times bigger than g, or in other words when g grows by 1, f grows by at most 111. It may seem that growing 111 times faster is not "approximately the same speed". And indeed the Big-O notation is not a very precise way to classify function convergence speed, which is why in mathematics we use the equivalence relationship (opens new window) when we want a precise estimation of speed. But for the purposes of separating algorithms in large speed classes, Big-O is enough. We don't need to separate functions that grow a fixed number of times faster than each other, but only functions that grow infinitely faster than each other. For instance if we take Now I need to make a side note : you might have noticed that if How is it used ? When comparing algorithm performance, we are interested in the number of operations that an algorithm performs. This is called time complexity. In this model, we consider that each basic operation (addition, multiplication, comparison, assignment, etc.) takes a fixed amount of time, and we count the number of such operations. We can usually express this number as a function of the size of the input, which we call n. And sadly, this number usually grows to infinity with n (if it doesn't, we say that the algorithm is O(1)). We separate our algorithms in big speed classes defined by Big-O : when we speak about a "O(n^2) algorithm", we mean that the number of operations it performs, expressed as a function of n, is a O(n^2). This says that our algorithm is approximately as fast as an algorithm that would do a number of operations equal to the square of the size of its input, or faster. The "or faster" part is there because I used Big-O instead of Big-Theta, but usually people will say Big-O to mean Big-Theta. When counting operations, we usually consider the worst case: for instance if we have a loop that can run at most n times and that contains 5 operations, the number of operations we count is 5n. It is also possible to consider the average case complexity. Quick note : a fast algorithm is one that performs few operations, so if the number of operations grows to infinity faster, then the algorithm is slower: O(n) is better than O(n^2). We are also sometimes interested in the space complexity of our algorithm. For this we consider the number of bytes in memory occupied by the algorithm as a function of the size of the input, and use Big-O the same way. |
Read Article
https://develop.horje.com/learn/1434/reference