Introduction
What do we mean by an ‘Algorithmic complexity’, a good (but not fully formal) definition would be - “How much resources does the algorithm use in relation to quantitative properties of its input”
And just to be clear, the definition of an algorithm: any well-defined procedure to solve a given problem.
So what kind of resources could we quantify from an input? Of course the runtime of an algorithm is important. How much memory/space an algorithm requires to operate, is often important as well. These are the main two resources that are looked upon when writing new algorithms - but we could also look at, # of comparisons, # of array accesses, # of arithmetic operations performed etc.
The quantitative properties of the input are important here as well, the time it takes to find the first 10 digits of is obviously going to be less than the first 100 000 digits of
Therefore, we can view the complexity as a mathematical function; Which takes an input of (size) n - maps it to the function T(n) that outputs the given runtime/space it takes for an input of n.
In pure mathematics (and Computer Science) this type of functions are called ‘Big O Notation’1
One thing to remember is - that in the real world the only thing that determines the output isn’t just the output. For example a input of n doesn’t always give T(n) for a given quicksort algorithm.
That’s why computer scientists use Worst-case, Best-case, and Average-case complexities.
As said before the exact runtime of a program depends on so many things - that’s why we introduce the so-called Asymptotic complexity. Which is the order of growth of the complexity function - which holds true for all complexity functions within the same ‘class’. This is often what computer scientists mean when they say that an algorithm has a complexity of .
A Computer Scientist Definition of Complexity
Let and be functions: has an order of growth of if: There are constants and such that:
What this means if we decipher the math; is eventually bounded by , after a certain point,
With this in our toolkit we can now use the O-notation:
Example 1
Say we have the function - I claim that the (Asymptotic) complexity of this function is
Proof
We need a and so that for
If we pick and say
Then for
Orders of growth
One with a background in Calculus soon realizes that order of this Big O functions will be the following:
Rules of (Asymptotic) Complexity
Some arithmetic rules for using the Big O Notation are the following:
Addition
Multiplication
And
How to find the Complexity of code
Now that we have understood properly what the Big O notation is and how we can operate with it - the next step is being able to find the complexity in code! How we do this properly is going through each line/operation in an algorithm and see what the complexity of each is. This is very time-consuming though - so, luckily, there are shortcuts.
Sequence of statements (arithmetic and logical operations, function calls, etc.), gives us addition between them.
func whats_the_complexity(int n): n = n + 5; // This takes O(1) n = n / 10; // This takes O(1) n = do_something(n, 100); // Say this take O(n)
return nThe total complexity of whats_the_complexity would in this case be:
and from our first rule that would mean a total (Asymptotic) complexity of, for
Nested loops gives multiplication
func whats_the_complexity(int[][] arr):
// Suppose arr is a n x x matrix
sum = 0;
for i in arr: // Takes O(n) for j in i: // Takes O(n) sum += j;
for i in arr: // Takes O(n) sum += i;
return nThe total complexity of whats_the_complexity would in this case be:
and from our first and second rule that would mean a total (Asymptotic) complexity of,
Relatives of O-notation
What we have gone through is only a part of a bigger set of notations. The ‘real’ definitions are the following:
Let and be functions
if:
Now let’s introduce:
if:
if:
This means: means and
What does this mean is
the function eventually has an upper bound the function eventually has a lower bound the function eventually has both a lower and upper bound
Conclusion
This concludes the first part of our complexity journey - complexities are, as we can see, a very powerful tool to identify what kind of algorithm we’re working with and to get a somewhat accurate answer about question one can have about programs, for example, runtime of a program.
Next part will be about dynamic arrays and how we use them - especially how to implement a stack and queues.