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 that outputs the given runtime/space it takes for an input of .
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 input. For example an input of doesn’t always give 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
Definition 1 (Order of growth)
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 these 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 n 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 sumThe 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:
Definition 2 (Relatives of O-notation)
Let and be functions.
if,
Now let’s introduce,
if,
if,
This means that means and .
What this means 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 questions 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 stacks and queues.