Part 2 - Complexity (1)

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 π\pi is obviously going to be less than the first 100 000 digits of π\pi

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 \dots.

A Computer Scientist Definition of Complexity

Let ff and gg be functions: ff has an order of growth of gg if: There are constants CC and n0n_0 such that:

f(n)C g(n) for nn0f(n) \leq C\ g(n) \text{ for } n \geq n_0

What this means if we decipher the math; f(n)f(n) is eventually bounded by C g(n)C\ g(n), after a certain point, n0n_0

With this in our toolkit we can now use the O-notation:

f(n)O(g(n))f(n) \in \mathcal{O}(g(n))
Example 1

Say we have the function f(n)=13n+37f(n) = 13n + 37 - I claim that the (Asymptotic) complexity of this function is O(n)\mathcal{O}(n)

Proof

We need a CC and n0n_0 so that 13n+37C n13n + 37 \leq C\ n for nn0n \geq n_0

If we pick C=14C = 14 and say n0=37n_0 = 37

Then 13n+3713n+n13n+3714n13n + 37 \leq 13n + n \Longleftrightarrow 13n + 37 \leq 14n for nn0n \geq n_0

Orders of growth

One with a background in Calculus soon realizes that order of this Big O functions will be the following:

O(1)O(log(n))O(n)O(n log(n))O(n2)O(2n)\mathcal{O}(1) \newline \mathcal{O}(log(n)) \newline \dots \newline \mathcal{O}(n) \newline \mathcal{O}(n\ log(n)) \newline \mathcal{O}(n^2) \newline \dots \newline \mathcal{O}(2^n) \newline \dots

Rules of (Asymptotic) Complexity

Some arithmetic rules for using the Big O Notation are the following:

Addition

O(f)+O(g)=O(f+g)=O(max(f,g))=max(O(f),O(g))\mathcal{O}(f) + \mathcal{O}(g) = \mathcal{O}(f + g) = \mathcal{O}(max(f,g)) = max(\mathcal{O}(f), \mathcal{O}(g))

Multiplication

O(f)×O(g)=O(f×g)\mathcal{O}(f) \times \mathcal{O}(g) = \mathcal{O}(f \times g)

And

C×O(f)=O(f)C \times \mathcal{O}(f) = \mathcal{O}(f)

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 n

The total complexity of whats_the_complexity would in this case be: O(1)+O(1)+O(n)\mathcal{O}(1) + \mathcal{O}(1) + \mathcal{O}(n) and from our first rule that would mean a total (Asymptotic) complexity of, O(n)\mathcal{O}(n) for n1n \geq 1

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 n

The total complexity of whats_the_complexity would in this case be: O(n)×O(n)+O(n)\mathcal{O}(n) \times \mathcal{O}(n) + \mathcal{O}(n) and from our first and second rule that would mean a total (Asymptotic) complexity of, O(n2)\mathcal{O}(n^2)

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 ff and gg be functions

f(n)O(g(n))f(n) \in \mathcal{O}(g(n)) if:

f(n)C g(n) for nn0f(n) \leq C\ g(n) \text{ for } n \geq n_0

Now let’s introduce:

f(n)Ω(g(n))f(n) \in \Omega(g(n)) if:

f(n)c g(n) for nn0f(n) \geq c\ g(n) \text{ for } n \geq n_0

f(n)Θ(g(n))f(n) \in \Theta(g(n)) if:

c g(n)f(n)C g(n) for nn0c\ g(n) \leq f(n) \leq C\ g(n) \text{ for } n \geq n_0

This means: f(n)Θ(g(n))f(n) \in \Theta(g(n)) means f(n)O(g(n))f(n) \in \mathcal{O}(g(n)) and f(n)Ω(g(n))f(n) \in \Omega(g(n))

What does this mean is

f(n)O(g(n))f(n) \in \mathcal{O}(g(n)) the function f(n)f(n) eventually has an upper bound f(n)Ω(g(n))f(n) \in \Omega(g(n)) the function f(n)f(n) eventually has a lower bound f(n)Θ(g(n))f(n) \in \Theta(g(n)) the function f(n)f(n) 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.

Footnotes

  1. Big O notation